diff --git a/client/app/components/dashboards/CreateDashboardDialog.jsx b/client/app/components/dashboards/CreateDashboardDialog.jsx index 0993e89721..cb6bb8caa9 100644 --- a/client/app/components/dashboards/CreateDashboardDialog.jsx +++ b/client/app/components/dashboards/CreateDashboardDialog.jsx @@ -1,6 +1,5 @@ import { trim } from "lodash"; import React, { useState } from "react"; -import { axios } from "@/services/axios"; import Modal from "antd/lib/modal"; import Input from "antd/lib/input"; import DynamicComponent from "@/components/DynamicComponent"; @@ -8,6 +7,7 @@ import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper"; import navigateTo from "@/components/ApplicationArea/navigateTo"; import recordEvent from "@/services/recordEvent"; import { policy } from "@/services/policy"; +import { Dashboard } from "@/services/dashboard"; function CreateDashboardDialog({ dialog }) { const [name, setName] = useState(""); @@ -25,9 +25,9 @@ function CreateDashboardDialog({ dialog }) { if (name !== "") { setSaveInProgress(true); - axios.post("api/dashboards", { name }).then(data => { + Dashboard.save({ name }).then(data => { dialog.close(); - navigateTo(`dashboard/${data.slug}?edit`); + navigateTo(`${data.url}?edit`); }); recordEvent("create", "dashboard"); } diff --git a/client/app/components/queries/AddToDashboardDialog.jsx b/client/app/components/queries/AddToDashboardDialog.jsx index 48e9139811..4510ee4170 100644 --- a/client/app/components/queries/AddToDashboardDialog.jsx +++ b/client/app/components/queries/AddToDashboardDialog.jsx @@ -38,7 +38,7 @@ function AddToDashboardDialog({ dialog, visualization }) { function addWidgetToDashboard() { // Load dashboard with all widgets - Dashboard.get({ slug: selectedDashboard.slug }) + Dashboard.get(selectedDashboard) .then(dashboard => { dashboard.addWidget(visualization); return dashboard; @@ -51,7 +51,7 @@ function AddToDashboardDialog({ dialog, visualization }) { notification.success( "Widget added to dashboard", - notification.close(key)}> + notification.close(key)}> {dashboard.name} diff --git a/client/app/pages/dashboards/DashboardList.jsx b/client/app/pages/dashboards/DashboardList.jsx index 153429c8cf..a3f0d0d5dd 100644 --- a/client/app/pages/dashboards/DashboardList.jsx +++ b/client/app/pages/dashboards/DashboardList.jsx @@ -46,7 +46,7 @@ class DashboardList extends React.Component { Columns.custom.sortable( (text, item) => ( - + {item.name} +
{!isEmpty(globalParameters) && (
@@ -145,35 +147,52 @@ DashboardComponent.propTypes = { dashboard: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types }; -function DashboardPage({ dashboardSlug, onError }) { +function DashboardPage({ dashboardSlug, dashboardId, onError }) { const [dashboard, setDashboard] = useState(null); const handleError = useImmutableCallback(onError); useEffect(() => { - Dashboard.get({ slug: dashboardSlug }) + Dashboard.get({ id: dashboardId, slug: dashboardSlug }) .then(dashboardData => { recordEvent("view", "dashboard", dashboardData.id); setDashboard(dashboardData); + + // if loaded by slug, update location url to use the id + if (!dashboardId) { + location.setPath(url.parse(dashboardData.url).pathname, true); + } }) .catch(handleError); - }, [dashboardSlug, handleError]); + }, [dashboardId, dashboardSlug, handleError]); return
{dashboard && }
; } DashboardPage.propTypes = { - dashboardSlug: PropTypes.string.isRequired, + dashboardSlug: PropTypes.string, + dashboardId: PropTypes.string, onError: PropTypes.func, }; DashboardPage.defaultProps = { + dashboardSlug: null, + dashboardId: null, onError: PropTypes.func, }; +// route kept for backward compatibility routes.register( - "Dashboards.ViewOrEdit", + "Dashboards.LegacyViewOrEdit", routeWithUserSession({ path: "/dashboard/:dashboardSlug", render: pageProps => , }) ); + +routes.register( + "Dashboards.ViewOrEdit", + routeWithUserSession({ + path: "/dashboards/:dashboardId([^-]+)(-.*)?", + render: pageProps => , + }) +); diff --git a/client/app/pages/dashboards/hooks/useDashboard.js b/client/app/pages/dashboards/hooks/useDashboard.js index 3fd9d8da85..07614ff68c 100644 --- a/client/app/pages/dashboards/hooks/useDashboard.js +++ b/client/app/pages/dashboards/hooks/useDashboard.js @@ -2,6 +2,7 @@ import { useState, useEffect, useMemo, useCallback, useRef } from "react"; import { isEmpty, includes, compact, map, has, pick, keys, extend, every, get } from "lodash"; import notification from "@/services/notification"; import location from "@/services/location"; +import url from "@/services/url"; import { Dashboard, collectDashboardFilters } from "@/services/dashboard"; import { currentUser } from "@/services/auth"; import recordEvent from "@/services/recordEvent"; @@ -63,15 +64,17 @@ function useDashboard(dashboardData) { const updateDashboard = useCallback( (data, includeVersion = true) => { setDashboard(currentDashboard => extend({}, currentDashboard, data)); - // for some reason the request uses the id as slug - data = { ...data, slug: dashboard.id }; + data = { ...data, id: dashboard.id }; if (includeVersion) { data = { ...data, version: dashboard.version }; } return Dashboard.save(data) - .then(updatedDashboard => - setDashboard(currentDashboard => extend({}, currentDashboard, pick(updatedDashboard, keys(data)))) - ) + .then(updatedDashboard => { + setDashboard(currentDashboard => extend({}, currentDashboard, pick(updatedDashboard, keys(data)))); + if (has(data, "name")) { + location.setPath(url.parse(updatedDashboard.url).pathname, true); + } + }) .catch(error => { const status = get(error, "response.status"); if (status === 403) { diff --git a/client/app/pages/home/Home.jsx b/client/app/pages/home/Home.jsx index 68c7e9e26b..4fbd14b4dc 100644 --- a/client/app/pages/home/Home.jsx +++ b/client/app/pages/home/Home.jsx @@ -119,7 +119,7 @@ function DashboardAndQueryFavoritesList() { `dashboard/${dashboard.slug}`} + itemUrl={dashboard => dashboard.url} emptyState={

diff --git a/client/app/services/dashboard.js b/client/app/services/dashboard.js index f347048ca0..d2f1806fa6 100644 --- a/client/app/services/dashboard.js +++ b/client/app/services/dashboard.js @@ -6,6 +6,8 @@ import { currentUser } from "@/services/auth"; import location from "@/services/location"; import { cloneParameter } from "@/services/parameters"; +export const urlForDashboard = ({ id, slug }) => `dashboards/${id}-${slug}`; + export function collectDashboardFilters(dashboard, queryResults, urlParams) { const filters = {}; _.each(queryResults, queryResult => { @@ -123,6 +125,11 @@ function calculateNewWidgetPosition(existingWidgets, newWidget) { export function Dashboard(dashboard) { _.extend(this, dashboard); + Object.defineProperty(this, "url", { + get: function() { + return urlForDashboard(this); + }, + }); } function prepareDashboardWidgets(widgets) { @@ -147,17 +154,23 @@ function transformResponse(data) { return data; } -const saveOrCreateUrl = data => (data.slug ? `api/dashboards/${data.slug}` : "api/dashboards"); +const saveOrCreateUrl = data => (data.id ? `api/dashboards/${data.id}` : "api/dashboards"); const DashboardService = { - get: ({ slug }) => axios.get(`api/dashboards/${slug}`).then(transformResponse), + get: ({ id, slug }) => { + const params = {}; + if (!id) { + params.legacy = null; + } + return axios.get(`api/dashboards/${id || slug}`, { params }).then(transformResponse); + }, getByToken: ({ token }) => axios.get(`api/dashboards/public/${token}`).then(transformResponse), save: data => axios.post(saveOrCreateUrl(data), data).then(transformResponse), - delete: ({ slug }) => axios.delete(`api/dashboards/${slug}`).then(transformResponse), + delete: ({ id }) => axios.delete(`api/dashboards/${id}`).then(transformResponse), query: params => axios.get("api/dashboards", { params }).then(transformResponse), recent: params => axios.get("api/dashboards/recent", { params }).then(transformResponse), favorites: params => axios.get("api/dashboards/favorites", { params }).then(transformResponse), - favorite: ({ slug }) => axios.post(`api/dashboards/${slug}/favorite`), - unfavorite: ({ slug }) => axios.delete(`api/dashboards/${slug}/favorite`), + favorite: ({ id }) => axios.post(`api/dashboards/${id}/favorite`), + unfavorite: ({ id }) => axios.delete(`api/dashboards/${id}/favorite`), }; _.extend(Dashboard, DashboardService); diff --git a/client/cypress/integration/dashboard/dashboard_spec.js b/client/cypress/integration/dashboard/dashboard_spec.js index 489a07ea5f..2410c51a1a 100644 --- a/client/cypress/integration/dashboard/dashboard_spec.js +++ b/client/cypress/integration/dashboard/dashboard_spec.js @@ -25,19 +25,19 @@ describe("Dashboard", () => { }); cy.wait("@NewDashboard").then(xhr => { - const slug = Cypress._.get(xhr, "response.body.slug"); - assert.isDefined(slug, "Dashboard api call returns slug"); + const id = Cypress._.get(xhr, "response.body.id"); + assert.isDefined(id, "Dashboard api call returns id"); cy.visit("/dashboards"); cy.getByTestId("DashboardLayoutContent").within(() => { - cy.getByTestId(slug).should("exist"); + cy.getByTestId(`DashboardId${id}`).should("exist"); }); }); }); it("archives dashboard", () => { - createDashboard("Foo Bar").then(({ slug }) => { - cy.visit(`/dashboard/${slug}`); + createDashboard("Foo Bar").then(({ id }) => { + cy.visit(`/dashboards/${id}`); cy.getByTestId("DashboardMoreButton").click(); @@ -52,7 +52,22 @@ describe("Dashboard", () => { cy.visit("/dashboards"); cy.getByTestId("DashboardLayoutContent").within(() => { - cy.getByTestId(slug).should("not.exist"); + cy.getByTestId(`DashboardId${id}`).should("not.exist"); + }); + }); + }); + + it("is accessible through multiple urls", () => { + cy.server(); + cy.route("GET", "api/dashboards/*").as("LoadDashboard"); + createDashboard("Dashboard multiple urls").then(({ id, slug }) => { + [`/dashboards/${id}`, `/dashboards/${id}-anything-here`, `/dashboard/${slug}`].forEach(url => { + cy.visit(url); + cy.wait("@LoadDashboard"); + cy.getByTestId(`DashboardId${id}Container`).should("exist"); + + // assert it always use the "/dashboards/{id}" path + cy.location("pathname").should("contain", `/dashboards/${id}`); }); }); }); @@ -61,9 +76,9 @@ describe("Dashboard", () => { before(function() { cy.login(); createDashboard("Foo Bar") - .then(({ slug, id }) => { - this.dashboardUrl = `/dashboard/${slug}`; - this.dashboardEditUrl = `/dashboard/${slug}?edit`; + .then(({ id }) => { + this.dashboardUrl = `/dashboards/${id}`; + this.dashboardEditUrl = `/dashboards/${id}?edit`; return addTextbox(id, "Hello World!").then(getWidgetTestId); }) .then(elTestId => { @@ -117,8 +132,8 @@ describe("Dashboard", () => { context("viewport width is at 767px", () => { before(function() { cy.login(); - createDashboard("Foo Bar").then(({ slug }) => { - this.dashboardUrl = `/dashboard/${slug}`; + createDashboard("Foo Bar").then(({ id }) => { + this.dashboardUrl = `/dashboards/${id}`; }); }); diff --git a/client/cypress/integration/dashboard/dashboard_tags_spec.js b/client/cypress/integration/dashboard/dashboard_tags_spec.js index 9d8ef131ae..c7dd91e649 100644 --- a/client/cypress/integration/dashboard/dashboard_tags_spec.js +++ b/client/cypress/integration/dashboard/dashboard_tags_spec.js @@ -4,7 +4,7 @@ import { expectTagsToContain, typeInTagsSelectAndSave } from "../../support/tags describe("Dashboard Tags", () => { beforeEach(function() { cy.login(); - createDashboard("Foo Bar").then(({ slug }) => cy.visit(`/dashboard/${slug}`)); + createDashboard("Foo Bar").then(({ id }) => cy.visit(`/dashboards/${id}`)); }); it("is possible to add and edit tags", () => { diff --git a/client/cypress/integration/dashboard/filters_spec.js b/client/cypress/integration/dashboard/filters_spec.js index 47091c600a..b05b8c3e6c 100644 --- a/client/cypress/integration/dashboard/filters_spec.js +++ b/client/cypress/integration/dashboard/filters_spec.js @@ -29,7 +29,7 @@ describe("Dashboard Filters", () => { .as("widget1TestId") .then(() => createQueryAndAddWidget(dashboard.id, queryData, { position: { col: 4 } })) .as("widget2TestId") - .then(() => cy.visit(`/dashboard/${dashboard.slug}`)); + .then(() => cy.visit(`/dashboards/${dashboard.id}`)); }); }); diff --git a/client/cypress/integration/dashboard/grid_compliant_widgets_spec.js b/client/cypress/integration/dashboard/grid_compliant_widgets_spec.js index 31a7361979..27fcdbafb5 100644 --- a/client/cypress/integration/dashboard/grid_compliant_widgets_spec.js +++ b/client/cypress/integration/dashboard/grid_compliant_widgets_spec.js @@ -10,8 +10,8 @@ describe("Grid compliant widgets", () => { cy.login(); cy.viewport(1215 + menuWidth, 800); createDashboard("Foo Bar") - .then(({ slug, id }) => { - this.dashboardUrl = `/dashboard/${slug}`; + .then(({ id }) => { + this.dashboardUrl = `/dashboards/${id}`; return addTextbox(id, "Hello World!").then(getWidgetTestId); }) .then(elTestId => { diff --git a/client/cypress/integration/dashboard/parameter_mapping_spec.js b/client/cypress/integration/dashboard/parameter_mapping_spec.js index 04dc58a439..4223f35dff 100644 --- a/client/cypress/integration/dashboard/parameter_mapping_spec.js +++ b/client/cypress/integration/dashboard/parameter_mapping_spec.js @@ -5,9 +5,9 @@ describe("Parameter Mapping", () => { beforeEach(function() { cy.login(); createDashboard("Foo Bar") - .then(({ slug, id }) => { + .then(({ id }) => { this.dashboardId = id; - this.dashboardUrl = `/dashboard/${slug}`; + this.dashboardUrl = `/dashboards/${id}`; }) .then(() => { const queryData = { diff --git a/client/cypress/integration/dashboard/sharing_spec.js b/client/cypress/integration/dashboard/sharing_spec.js index adba653603..4c7369dc4a 100644 --- a/client/cypress/integration/dashboard/sharing_spec.js +++ b/client/cypress/integration/dashboard/sharing_spec.js @@ -6,9 +6,9 @@ import { editDashboard, shareDashboard, createQueryAndAddWidget } from "../../su describe("Dashboard Sharing", () => { beforeEach(function() { cy.login(); - createDashboard("Foo Bar").then(({ slug, id }) => { + createDashboard("Foo Bar").then(({ id }) => { this.dashboardId = id; - this.dashboardUrl = `/dashboard/${slug}`; + this.dashboardUrl = `/dashboards/${id}`; }); }); diff --git a/client/cypress/integration/dashboard/textbox_spec.js b/client/cypress/integration/dashboard/textbox_spec.js index 7e5beffa54..3e4c7910a8 100644 --- a/client/cypress/integration/dashboard/textbox_spec.js +++ b/client/cypress/integration/dashboard/textbox_spec.js @@ -6,9 +6,9 @@ import { getWidgetTestId, editDashboard } from "../../support/dashboard"; describe("Textbox", () => { beforeEach(function() { cy.login(); - createDashboard("Foo Bar").then(({ slug, id }) => { + createDashboard("Foo Bar").then(({ id }) => { this.dashboardId = id; - this.dashboardUrl = `/dashboard/${slug}`; + this.dashboardUrl = `/dashboards/${id}`; }); }); diff --git a/client/cypress/integration/dashboard/widget_spec.js b/client/cypress/integration/dashboard/widget_spec.js index 2eff9e4f21..60501f2ab7 100644 --- a/client/cypress/integration/dashboard/widget_spec.js +++ b/client/cypress/integration/dashboard/widget_spec.js @@ -6,9 +6,9 @@ import { createQueryAndAddWidget, editDashboard, resizeBy } from "../../support/ describe("Widget", () => { beforeEach(function() { cy.login(); - createDashboard("Foo Bar").then(({ slug, id }) => { + createDashboard("Foo Bar").then(({ id }) => { this.dashboardId = id; - this.dashboardUrl = `/dashboard/${slug}`; + this.dashboardUrl = `/dashboards/${id}`; }); }); diff --git a/client/cypress/integration/visualizations/pivot_spec.js b/client/cypress/integration/visualizations/pivot_spec.js index 54d4cf53ab..1e2f153ce3 100644 --- a/client/cypress/integration/visualizations/pivot_spec.js +++ b/client/cypress/integration/visualizations/pivot_spec.js @@ -143,7 +143,7 @@ describe("Pivot", () => { createDashboard("Pivot Visualization") .then(dashboard => { - this.dashboardUrl = `/dashboard/${dashboard.slug}`; + this.dashboardUrl = `/dashboards/${dashboard.id}`; return cy.all( pivotTables.map(pivot => () => createVisualization(this.queryId, "PIVOT", pivot.name, pivot.options).then(visualization => diff --git a/client/cypress/integration/visualizations/sankey_sunburst_spec.js b/client/cypress/integration/visualizations/sankey_sunburst_spec.js index 5bb8eda9e9..190b7c96c2 100644 --- a/client/cypress/integration/visualizations/sankey_sunburst_spec.js +++ b/client/cypress/integration/visualizations/sankey_sunburst_spec.js @@ -99,7 +99,7 @@ describe("Sankey and Sunburst", () => { it("takes a snapshot with Sunburst (1 - 5 stages)", function() { createDashboard("Sunburst Visualization").then(dashboard => { - this.dashboardUrl = `/dashboard/${dashboard.slug}`; + this.dashboardUrl = `/dashboards/${dashboard.id}`; return cy .all( STAGES_WIDGETS.map(sunburst => () => @@ -123,7 +123,7 @@ describe("Sankey and Sunburst", () => { it("takes a snapshot with Sankey (1 - 5 stages)", function() { createDashboard("Sankey Visualization").then(dashboard => { - this.dashboardUrl = `/dashboard/${dashboard.slug}`; + this.dashboardUrl = `/dashboards/${dashboard.id}`; return cy .all( STAGES_WIDGETS.map(sankey => () => diff --git a/redash/handlers/api.py b/redash/handlers/api.py index 8f7cc1a369..1292d63100 100644 --- a/redash/handlers/api.py +++ b/redash/handlers/api.py @@ -127,7 +127,7 @@ def json_representation(data, code, headers=None): api.add_org_resource(DashboardListResource, "/api/dashboards", endpoint="dashboards") api.add_org_resource( - DashboardResource, "/api/dashboards/", endpoint="dashboard" + DashboardResource, "/api/dashboards/", endpoint="dashboard" ) api.add_org_resource( PublicDashboardResource, diff --git a/redash/handlers/dashboards.py b/redash/handlers/dashboards.py index 1575fe2e28..cfc710930f 100644 --- a/redash/handlers/dashboards.py +++ b/redash/handlers/dashboards.py @@ -115,11 +115,11 @@ def post(self): class DashboardResource(BaseResource): @require_permission("list_dashboards") - def get(self, dashboard_slug=None): + def get(self, dashboard_id=None): """ Retrieves a dashboard. - :qparam string slug: Slug of dashboard to retrieve. + :qparam number id: Id of dashboard to retrieve. .. _dashboard-response-label: @@ -149,9 +149,12 @@ def get(self, dashboard_slug=None): :>json string widget.created_at: ISO format timestamp for widget creation :>json string widget.updated_at: ISO format timestamp for last widget modification """ - dashboard = get_object_or_404( - models.Dashboard.get_by_slug_and_org, dashboard_slug, self.current_org - ) + if request.args.get("legacy") is not None: + fn = models.Dashboard.get_by_slug_and_org + else: + fn = models.Dashboard.get_by_id_and_org + + dashboard = get_object_or_404(fn, dashboard_id, self.current_org) response = DashboardSerializer( dashboard, with_widgets=True, user=self.current_user ).serialize() @@ -175,11 +178,11 @@ def get(self, dashboard_slug=None): return response @require_permission("edit_dashboard") - def post(self, dashboard_slug): + def post(self, dashboard_id): """ Modifies a dashboard. - :qparam string slug: Slug of dashboard to retrieve. + :qparam number id: Id of dashboard to retrieve. Responds with the updated :ref:`dashboard `. @@ -188,7 +191,7 @@ def post(self, dashboard_slug): """ dashboard_properties = request.get_json(force=True) # TODO: either convert all requests to use slugs or ids - dashboard = models.Dashboard.get_by_id_and_org(dashboard_slug, self.current_org) + dashboard = models.Dashboard.get_by_id_and_org(dashboard_id, self.current_org) require_object_modify_permission(dashboard, self.current_user) @@ -231,17 +234,15 @@ def post(self, dashboard_slug): return result @require_permission("edit_dashboard") - def delete(self, dashboard_slug): + def delete(self, dashboard_id): """ Archives a dashboard. - :qparam string slug: Slug of dashboard to retrieve. + :qparam number id: Id of dashboard to retrieve. Responds with the archived :ref:`dashboard `. """ - dashboard = models.Dashboard.get_by_slug_and_org( - dashboard_slug, self.current_org - ) + dashboard = models.Dashboard.get_by_id_and_org(dashboard_id, self.current_org) dashboard.is_archived = True dashboard.record_changes(changed_by=self.current_user) models.db.session.add(dashboard) diff --git a/redash/handlers/favorites.py b/redash/handlers/favorites.py index 71ac3a20b8..71a0ac3db1 100644 --- a/redash/handlers/favorites.py +++ b/redash/handlers/favorites.py @@ -51,7 +51,7 @@ def delete(self, query_id): class DashboardFavoriteResource(BaseResource): def post(self, object_id): dashboard = get_object_or_404( - models.Dashboard.get_by_slug_and_org, object_id, self.current_org + models.Dashboard.get_by_id_and_org, object_id, self.current_org ) fav = models.Favorite( org_id=self.current_org.id, object=dashboard, user=self.current_user @@ -76,7 +76,7 @@ def post(self, object_id): def delete(self, object_id): dashboard = get_object_or_404( - models.Dashboard.get_by_slug_and_org, object_id, self.current_org + models.Dashboard.get_by_id_and_org, object_id, self.current_org ) models.Favorite.query.filter( models.Favorite.object == dashboard, diff --git a/redash/models/__init__.py b/redash/models/__init__.py index 1bd2fab77f..4e7b97cb90 100644 --- a/redash/models/__init__.py +++ b/redash/models/__init__.py @@ -5,7 +5,7 @@ import numbers import pytz -from sqlalchemy import distinct, or_, and_, UniqueConstraint +from sqlalchemy import distinct, or_, and_, UniqueConstraint, cast from sqlalchemy.dialects import postgresql from sqlalchemy.event import listens_for from sqlalchemy.ext.hybrid import hybrid_property @@ -1099,6 +1099,10 @@ class Dashboard(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model def __str__(self): return "%s=%s" % (self.id, self.name) + @property + def name_as_slug(self): + return utils.slugify(self.name) + @classmethod def all(cls, org, group_ids, user_id): query = ( diff --git a/redash/serializers/__init__.py b/redash/serializers/__init__.py index b82c1b9f17..83b1fa266b 100644 --- a/redash/serializers/__init__.py +++ b/redash/serializers/__init__.py @@ -245,7 +245,7 @@ def serialize_dashboard(obj, with_widgets=False, user=None, with_favorite_state= d = { "id": obj.id, - "slug": obj.slug, + "slug": obj.name_as_slug, "name": obj.name, "user_id": obj.user_id, "user": { diff --git a/tests/handlers/test_dashboards.py b/tests/handlers/test_dashboards.py index b60fe627a9..f89e553b4b 100644 --- a/tests/handlers/test_dashboards.py +++ b/tests/handlers/test_dashboards.py @@ -53,7 +53,17 @@ def test_search_term(self): class TestDashboardResourceGet(BaseTestCase): def test_get_dashboard(self): d1 = self.factory.create_dashboard() - rv = self.make_request("get", "/api/dashboards/{0}".format(d1.slug)) + rv = self.make_request("get", "/api/dashboards/{0}".format(d1.id)) + self.assertEqual(rv.status_code, 200) + + expected = serialize_dashboard(d1, with_widgets=True, with_favorite_state=False) + actual = json_loads(rv.data) + + self.assertResponseEqual(expected, actual) + + def test_get_dashboard_with_slug(self): + d1 = self.factory.create_dashboard() + rv = self.make_request("get", "/api/dashboards/{0}?legacy".format(d1.slug)) self.assertEqual(rv.status_code, 200) expected = serialize_dashboard(d1, with_widgets=True, with_favorite_state=False) @@ -76,13 +86,13 @@ def test_get_dashboard_filters_unauthorized_widgets(self): dashboard.layout = "[[{}, {}]]".format(widget.id, restricted_widget.id) db.session.commit() - rv = self.make_request("get", "/api/dashboards/{0}".format(dashboard.slug)) + rv = self.make_request("get", "/api/dashboards/{0}".format(dashboard.id)) self.assertEqual(rv.status_code, 200) self.assertTrue(rv.json["widgets"][0]["restricted"]) self.assertNotIn("restricted", rv.json["widgets"][1]) def test_get_non_existing_dashboard(self): - rv = self.make_request("get", "/api/dashboards/not_existing") + rv = self.make_request("get", "/api/dashboards/-1") self.assertEqual(rv.status_code, 404) @@ -156,10 +166,10 @@ class TestDashboardResourceDelete(BaseTestCase): def test_delete_dashboard(self): d = self.factory.create_dashboard() - rv = self.make_request("delete", "/api/dashboards/{0}".format(d.slug)) + rv = self.make_request("delete", "/api/dashboards/{0}".format(d.id)) self.assertEqual(rv.status_code, 200) - d = Dashboard.get_by_slug_and_org(d.slug, d.org) + d = Dashboard.get_by_id_and_org(d.id, d.org) self.assertTrue(d.is_archived)