Compare commits

..
Author SHA1 Message Date
Elizabeth Thompson 271cdd4564 fix(examples): replace deprecated granularity_sqla in YAML chart configs
Self-review on the prior commit caught that the same slices also ship
as YAML fixtures under examples/usa_births_names/charts and
examples/world_health/charts, loaded unconditionally by
load_examples_from_configs() on every `superset load_examples` run.
Those still used the old granularity_sqla key, so the deprecation
warning would keep firing via that path even after the .py rename.
Mirrors the scope of the merged precedent (apache/superset#43422),
which covered both the .py and .yaml sides of the same pattern.
2026-08-23 16:08:09 +00:00
Elizabeth Thompson 2ebddb2e98 fix(examples): replace deprecated granularity_sqla with granularity
Superset.common.query_object.DEPRECATED_FIELDS logs a warning and
performs a runtime migration whenever granularity_sqla is present in
form_data. The birth_names and world_bank example datasets still
seeded slices using the old field name, triggering this warning on
every load. get_slice_json() is a plain dict merge with no schema
validation, so the rename is a straight drop-in with no behavior
change once the examples are (re)loaded.
2026-08-23 16:04:18 +00:00
39 changed files with 65 additions and 324 deletions
@@ -259,21 +259,6 @@ describe('isUserEditorOrAdmin', () => {
test('returns false when editors is omitted', () => {
expect(isUserEditorOrAdmin(outsiderUser)).toEqual(false);
});
test('returns true when the user is granted editorship only through extra_editors', () => {
expect(isUserEditorOrAdmin(editorUser, [], [10])).toEqual(true);
});
test('unions editors and extra_editors rather than preferring one', () => {
const nonMatchingSubject: Subject = { id: 999, label: 'Other', type: 1 };
expect(isUserEditorOrAdmin(editorUser, [nonMatchingSubject], [10])).toEqual(
true,
);
});
test('returns false when extra_editors names other subjects', () => {
expect(isUserEditorOrAdmin(editorUser, [], [999])).toEqual(false);
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
@@ -55,6 +55,9 @@ export const isUserInSubjects = (
);
};
const isUserInEditors = (editors: Subject[] = []): boolean =>
isUserInSubjects(editors);
export const isUserAdmin = (
user?: UserWithPermissionsAndRoles | UndefinedUser,
) =>
@@ -63,12 +66,10 @@ export const isUserAdmin = (
role => role.toLowerCase() === ADMIN_ROLE_NAME.toLowerCase(),
);
/** `extraEditors` is editorship granted via a deployment's EXTRA_EDITORS_RESOLVER. */
export const isUserEditorOrAdmin = (
user?: UserWithPermissionsAndRoles | UndefinedUser,
editors: Subject[] = [],
extraEditors?: SubjectRef[] | null,
): boolean => isUserInSubjects(editors, extraEditors) || isUserAdmin(user);
): boolean => isUserInEditors(editors) || isUserAdmin(user);
/**
* Editorship of *dashboard*, matching the server's `is_editor`: the explicit
@@ -29,7 +29,22 @@ import {
} from 'src/utils/localStorageHelpers';
import { SamplesPane, useResultsPane } from './components';
import { DataTablesPaneProps, ResultTypes } from './types';
import { getStaleResultsTabFallback } from './utils';
/**
* A mixed chart can be reconfigured to return fewer result panes than before
* (e.g. dropping a query), which removes the corresponding results tab. If the
* selected tab was one of those, the active key goes stale and the data panel
* renders blank until the user reselects a valid tab. Returns the first
* results tab to fall back to in that case, otherwise undefined.
*/
export const getStaleResultsTabFallback = (
activeTabKey: string,
resultsTabKeys: string[],
): string | undefined =>
activeTabKey.startsWith(ResultTypes.Results) &&
!resultsTabKeys.includes(activeTabKey)
? ResultTypes.Results
: undefined;
const StyledDiv = styled.div`
${() => `
@@ -20,15 +20,22 @@ import { t } from '@apache-superset/core/translation';
import { styled } from '@apache-superset/core/theme';
import Tabs from '@superset-ui/core/components/Tabs';
import { ResultTypes, ResultsPaneProps } from '../types';
import { getStaleResultsTabFallback } from '../utils';
import { useResultsPane } from './useResultsPane';
import { useEffect, useState } from 'react';
import { useState } from 'react';
const Wrapper = styled.div`
display: flex;
flex-direction: column;
height: 100%;
.ant-tabs {
height: 100%;
}
.ant-tabs-body {
height: 100%;
}
.ant-tabs-content {
display: flex;
flex-direction: column;
@@ -79,25 +86,9 @@ export const ResultsPaneOnDashboard = ({
};
});
const resultsTabFallback = getStaleResultsTabFallback(
activeTabKey,
items.map(({ key }) => key),
);
useEffect(() => {
if (resultsTabFallback) {
setActiveTabKey(resultsTabFallback);
}
}, [resultsTabFallback]);
return (
<Wrapper>
<Tabs
fullHeight
activeKey={activeTabKey}
onChange={setActiveTabKey}
items={items}
/>
<Tabs activeKey={activeTabKey} onChange={setActiveTabKey} items={items} />
</Wrapper>
);
};
@@ -20,34 +20,14 @@ import fetchMock from 'fetch-mock';
import {
screen,
render,
act,
waitForElementToBeRemoved,
waitFor,
} from 'spec/helpers/testing-library';
import { ChartMetadata, ChartPlugin, VizType } from '@superset-ui/core';
import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact';
import Tabs from '@superset-ui/core/components/Tabs';
import { ResultsPaneOnDashboard } from '../components';
import { useResultsPane } from '../components/useResultsPane';
import { createResultsPaneOnDashboardProps } from './fixture';
// `fullHeight`'s CSS isn't testable under jsdom (no `importSource` for the
// `css` prop in jest's babel config), so spy on call args instead.
jest.mock('@superset-ui/core/components/Tabs', () => {
const actual = jest.requireActual('@superset-ui/core/components/Tabs');
return { __esModule: true, ...actual, default: jest.fn(actual.default) };
});
// Wraps the real hook; only overridden below to avoid mounting a second
// real AG Grid instance, which jsdom doesn't support.
jest.mock('../components/useResultsPane', () => {
const actual = jest.requireActual('../components/useResultsPane');
return {
__esModule: true,
useResultsPane: jest.fn(actual.useResultsPane),
};
});
beforeAll(() => {
setupAGGridModules();
});
@@ -126,10 +106,6 @@ describe('ResultsPaneOnDashboard', () => {
expect(
await findByText('No results were returned for this query'),
).toBeVisible();
expect(Tabs).toHaveBeenCalledWith(
expect.objectContaining({ fullHeight: true }),
expect.anything(),
);
});
test('render errorMessage', async () => {
@@ -243,36 +219,4 @@ describe('ResultsPaneOnDashboard', () => {
expect(tab2).toBeVisible();
expect(tab3).toBeNull();
});
test('falls back to the first results tab when the active one disappears', async () => {
const mockedUseResultsPane = useResultsPane as jest.Mock;
mockedUseResultsPane.mockReturnValue([<div key="a" />, <div key="b" />]);
const props = createResultsPaneOnDashboardProps({ sliceId: 999 });
const { rerender } = render(<ResultsPaneOnDashboard {...props} />, {
useRedux: true,
});
const latestTabsProps = () => {
const { calls } = (Tabs as unknown as jest.Mock).mock;
return calls[calls.length - 1][0];
};
expect(latestTabsProps().items.map((i: { key: string }) => i.key)).toEqual([
'results',
'results 2',
]);
act(() => {
latestTabsProps().onChange('results 2');
});
expect(latestTabsProps().activeKey).toBe('results 2');
// A mixed chart dropped from two query results to one, removing "results 2"
mockedUseResultsPane.mockReturnValue([<div key="a" />]);
rerender(<ResultsPaneOnDashboard {...props} />);
await waitFor(() => {
expect(latestTabsProps().activeKey).toBe('results');
});
});
});
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { getStaleResultsTabFallback } from '../utils';
import { getStaleResultsTabFallback } from '../DataTablesPane';
import { ResultTypes } from '../types';
test('keeps the active tab when it still exists', () => {
@@ -1,35 +0,0 @@
/**
* 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 { ResultTypes } from './types';
/**
* A mixed chart can be reconfigured to return fewer result panes than before
* (e.g. dropping a query), which removes the corresponding results tab. If the
* selected tab was one of those, the active key goes stale and the data panel
* renders blank until the user reselects a valid tab. Returns the first
* results tab to fall back to in that case, otherwise undefined.
*/
export const getStaleResultsTabFallback = (
activeTabKey: string,
resultsTabKeys: string[],
): string | undefined =>
activeTabKey.startsWith(ResultTypes.Results) &&
!resultsTabKeys.includes(activeTabKey)
? ResultTypes.Results
: undefined;
@@ -96,11 +96,7 @@ export default function ChartCard({
const canEdit = hasPerm('can_write');
const canDelete = hasPerm('can_write');
const canExport = hasPerm('can_export');
const allowEdit = isUserEditorOrAdmin(
user,
chart.editors,
chart.extra_editors,
);
const allowEdit = isUserEditorOrAdmin(user, chart.editors);
const menuItems: MenuItem[] = [];
if (canEdit) {
@@ -83,11 +83,7 @@ function DashboardCard({
const canEdit = hasPerm('can_write');
const canDelete = hasPerm('can_write');
const canExport = hasPerm('can_export');
const allowEdit = isUserEditorOrAdmin(
user,
dashboard.editors,
dashboard.extra_editors,
);
const allowEdit = isUserEditorOrAdmin(user, dashboard.editors);
const digest = dashboard.changed_on_utc || dashboard.changed_on;
const thumbnailUrl =
isFeatureEnabled(FeatureFlag.Thumbnails) && dashboard.id && digest
@@ -650,11 +650,7 @@ function ChartList(props: ChartListProps) {
},
{
Cell: ({ row: { original } }: CellProps<Chart>) => {
const allowEdit = isUserEditorOrAdmin(
user,
original.editors,
original.extra_editors,
);
const allowEdit = isUserEditorOrAdmin(user, original.editors);
const openEditModal = () => openChartEditModal(original);
const handleExport = () => handleBulkChartExport([original]);
if (!canEdit && !canDelete && !canExport) {
@@ -122,8 +122,6 @@ export interface Dashboard {
description?: string;
thumbnail_url?: string | null;
editors?: Subject[];
// Bare subject ids from a deployment's EXTRA_EDITORS_RESOLVER.
extra_editors?: number[];
viewers?: Subject[];
tags: TagType[];
created_by: object;
@@ -507,11 +505,7 @@ function DashboardList(props: DashboardListProps) {
},
{
Cell: ({ row: { original } }: CellProps<Dashboard>) => {
const allowEdit = isUserEditorOrAdmin(
user,
original.editors,
original.extra_editors,
);
const allowEdit = isUserEditorOrAdmin(user, original.editors);
const handleDelete = () =>
handleDashboardDelete(
original,
-2
View File
@@ -45,8 +45,6 @@ export interface Chart {
cache_timeout: number | null;
thumbnail_url?: string;
editors?: Subject[];
// Bare subject ids from a deployment's EXTRA_EDITORS_RESOLVER.
extra_editors?: number[];
viewers?: Subject[];
tags?: TagType[];
last_saved_at?: string;
@@ -67,8 +67,6 @@ export interface Dashboard {
url: string;
thumbnail_url?: string | null;
editors?: Subject[];
// Bare subject ids from a deployment's EXTRA_EDITORS_RESOLVER.
extra_editors?: number[];
viewers?: Subject[];
loading?: boolean;
}
+1 -13
View File
@@ -92,10 +92,7 @@ from superset.exceptions import (
)
from superset.extensions import event_logger, security_manager
from superset.models.slice import Slice
from superset.security.manager import (
get_extra_editor_subject_ids,
get_extra_editors_by_pk,
)
from superset.security.manager import get_extra_editor_subject_ids
from superset.subjects.filters import (
FilterRelatedSubjects,
subject_type_filter,
@@ -413,15 +410,6 @@ class ChartRestApi(SoftDeleteApiMixin, BaseSupersetModelRestApi):
except ChartNotFoundError:
return self.response_404()
def pre_get_list(self, data: dict[str, Any]) -> None:
"""Attach ``extra_editors`` to each row, matching the single-object GET."""
super().pre_get_list(data)
ids = data.get("ids", [])
extra_editors_by_id = get_extra_editors_by_pk(Slice, ids)
for row, row_id in zip(data.get("result", []), ids, strict=False):
if row_id in extra_editors_by_id:
row["extra_editors"] = extra_editors_by_id[row_id]
@expose("/<pk>/deck_layers/", methods=("GET",))
@protect()
@safe
+1 -13
View File
@@ -142,10 +142,7 @@ from superset.extensions import event_logger, security_manager
from superset.models.dashboard import Dashboard
from superset.models.embedded_dashboard import EmbeddedDashboard
from superset.security.guest_token import GuestUser
from superset.security.manager import (
get_extra_editor_subject_ids,
get_extra_editors_by_pk,
)
from superset.security.manager import get_extra_editor_subject_ids
from superset.subjects.filters import (
FilterRelatedSubjects,
subject_type_filter,
@@ -436,15 +433,6 @@ class DashboardRestApi(
"""
return super().get_list(**kwargs)
def pre_get_list(self, data: dict[str, Any]) -> None:
"""Attach ``extra_editors`` to each row, matching the single-object GET."""
super().pre_get_list(data)
ids = data.get("ids", [])
extra_editors_by_id = get_extra_editors_by_pk(Dashboard, ids)
for row, row_id in zip(data.get("result", []), ids, strict=False):
if row_id in extra_editors_by_id:
row["extra_editors"] = extra_editors_by_id[row_id]
list_select_columns = list_columns + ["changed_on", "created_on", "changed_by_fk"]
order_columns = [
"changed_by.first_name",
+7 -7
View File
@@ -176,7 +176,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
"compare_lag": "10",
"compare_suffix": "o10Y",
"limit": "25",
"granularity_sqla": "ds",
"granularity": "ds",
"groupby": [],
"row_limit": current_app.config["ROW_LIMIT"],
"time_range": "100 years ago : now",
@@ -213,7 +213,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
params=get_slice_json(
defaults,
viz_type="big_number",
granularity_sqla="ds",
granularity="ds",
compare_lag="5",
compare_suffix="over 5Y",
metric=metric,
@@ -237,7 +237,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
defaults,
viz_type="echarts_timeseries_line",
groupby=["name"],
granularity_sqla="ds",
granularity="ds",
rich_tooltip=True,
show_legend=True,
metrics=metrics,
@@ -420,7 +420,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
}
],
metrics_b=["sum__num"],
granularity_sqla="ds",
granularity="ds",
yAxisIndex=0,
yAxisIndexB=1,
),
@@ -474,7 +474,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
"label": "SUM(num_california)",
},
viz_type="big_number_total",
granularity_sqla="ds",
granularity="ds",
),
editors=[],
),
@@ -496,7 +496,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
}
],
viz_type="echarts_timeseries_line",
granularity_sqla="ds",
granularity="ds",
groupby=["name"],
series_limit_metric={
"expressionType": "SIMPLE",
@@ -542,7 +542,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
defaults,
metric=metric,
viz_type="big_number_total",
granularity_sqla="ds",
granularity="ds",
adhoc_filters=[gen_filter("gender", "girl")],
subheader="total female participants",
),
@@ -28,7 +28,7 @@ params:
subject: gender
compare_lag: '10'
compare_suffix: o10Y
granularity_sqla: ds
granularity: ds
groupby: []
limit: '100'
markup_type: markdown
@@ -28,7 +28,7 @@ params:
subject: gender
compare_lag: '10'
compare_suffix: o10Y
granularity_sqla: ds
granularity: ds
groupby:
- name
limit: '25'
@@ -22,7 +22,7 @@ description: null
params:
compare_lag: '10'
compare_suffix: o10Y
granularity_sqla: ds
granularity: ds
groupby:
- gender
limit: '25'
@@ -30,7 +30,7 @@ params:
subject: state
compare_lag: '10'
compare_suffix: o10Y
granularity_sqla: ds
granularity: ds
groupby:
- state
limit: '25'
@@ -28,7 +28,7 @@ params:
subject: gender
compare_lag: '10'
compare_suffix: o10Y
granularity_sqla: ds
granularity: ds
groupby: []
limit: '100'
markup_type: markdown
@@ -28,7 +28,7 @@ params:
subject: gender
compare_lag: '10'
compare_suffix: o10Y
granularity_sqla: ds
granularity: ds
groupby:
- name
limit: '25'
@@ -22,7 +22,7 @@ description: null
params:
compare_lag: '5'
compare_suffix: over 5Y
granularity_sqla: ds
granularity: ds
groupby: []
limit: '25'
markup_type: markdown
@@ -22,7 +22,7 @@ description: null
params:
compare_lag: '10'
compare_suffix: o10Y
granularity_sqla: ds
granularity: ds
groupby: []
groupbyColumns:
- state
@@ -29,7 +29,7 @@ params:
compare_lag: '10'
compare_suffix: o10Y
comparison_type: values
granularity_sqla: ds
granularity: ds
groupby:
- name
limit: 10
@@ -29,7 +29,7 @@ params:
compare_lag: '10'
compare_suffix: o10Y
comparison_type: values
granularity_sqla: ds
granularity: ds
groupby:
- name
limit: 10
@@ -22,7 +22,7 @@ description: null
params:
compare_lag: '10'
compare_suffix: o10Y
granularity_sqla: ds
granularity: ds
groupby:
- name
limit: '25'
+1 -1
View File
@@ -154,7 +154,7 @@ def create_slices(tbl: BaseDatasource) -> list[Slice]:
"compare_lag": "10",
"compare_suffix": "o10Y",
"limit": "25",
"granularity_sqla": "year",
"granularity": "year",
"groupby": [],
"row_limit": current_app.config["ROW_LIMIT"],
"since": "2014-01-01",
@@ -24,7 +24,7 @@ params:
compare_suffix: o10Y
country_fieldtype: cca3
entity: country_code
granularity_sqla: year
granularity: year
groupby:
- region
limit: '25'
@@ -24,7 +24,7 @@ params:
compare_suffix: o10Y
country_fieldtype: cca3
entity: country_code
granularity_sqla: year
granularity: year
groupby:
- country_name
limit: '25'
@@ -43,7 +43,7 @@ params:
subject: country_code
color_scheme: supersetColors
entity: country_name
granularity_sqla: year
granularity: year
legendOrientation: top
legendType: scroll
max_bubble_size: '50'
@@ -24,7 +24,7 @@ params:
compare_suffix: o10Y
country_fieldtype: cca3
entity: country_code
granularity_sqla: year
granularity: year
groupby:
- country_name
limit: '25'
@@ -24,7 +24,7 @@ params:
compare_suffix: o10Y
country_fieldtype: cca3
entity: country_code
granularity_sqla: year
granularity: year
groupby: []
limit: '25'
markup_type: markdown
@@ -27,7 +27,7 @@ params:
compare_suffix: o10Y
country_fieldtype: cca3
entity: country_code
granularity_sqla: year
granularity: year
groupby: []
limit: '25'
markup_type: markdown
@@ -24,7 +24,7 @@ params:
compare_suffix: o10Y
country_fieldtype: cca3
entity: country_code
granularity_sqla: year
granularity: year
groupby:
- region
- country_code
@@ -24,7 +24,7 @@ params:
compare_suffix: o10Y
country_fieldtype: cca3
entity: country_code
granularity_sqla: year
granularity: year
groupby:
- region
limit: '25'
@@ -24,7 +24,7 @@ params:
compare_suffix: over 10Y
country_fieldtype: cca3
entity: country_code
granularity_sqla: year
granularity: year
groupby: []
limit: '25'
markup_type: markdown
-30
View File
@@ -170,36 +170,6 @@ def get_extra_editor_subject_ids(resource: Model) -> list[int]:
return subject_ids
def get_extra_editors_by_pk(
model_cls: type[Model], primary_keys: list[Any]
) -> dict[Any, list[int]]:
"""
Resolve extra editor subject IDs for a batch of resources, keyed by
primary key. List responses only have serialized rows, not model
instances, so this re-queries the page's rows in one batched query.
"""
if not primary_keys or not (
has_app_context() and current_app.config.get("EXTRA_EDITORS_RESOLVER")
):
return {}
# pylint: disable=import-outside-toplevel
from superset import db
from superset.models.helpers import SKIP_VISIBILITY_FILTER_CLASSES
pk_col = inspect(model_cls).primary_key[0]
resources = (
db.session.query(model_cls)
.execution_options(**{SKIP_VISIBILITY_FILTER_CLASSES: {model_cls}})
.filter(pk_col.in_(primary_keys))
.all()
)
return {
getattr(resource, pk_col.name): get_extra_editor_subject_ids(resource)
for resource in resources
}
def _render_permission_instructions_link(
*,
datasource_id: str = "",
@@ -922,55 +922,6 @@ class TestDashboardApi(ApiEditorsTestCaseMixin, InsertChartMixin, SupersetTestCa
db.session.delete(dashboard)
db.session.commit()
def test_get_dashboards_list_omits_extra_editors_by_default(self):
"""No EXTRA_EDITORS_RESOLVER configured: list rows omit extra_editors."""
admin = self.get_user("admin")
dashboard = self.insert_dashboard(
"no_extra_editors_list_dashboard",
"no-extra-editors-list-dashboard",
[admin.id],
)
try:
self.login(ADMIN_USERNAME)
rv = self.client.get("api/v1/dashboard/")
assert rv.status_code == 200
data = json.loads(rv.data.decode("utf-8"))
row = next(
d
for d in data["result"]
if d["dashboard_title"] == dashboard.dashboard_title
)
assert "extra_editors" not in row
finally:
db.session.delete(dashboard)
db.session.commit()
@with_config({"EXTRA_EDITORS_RESOLVER": lambda resource: [123]})
def test_get_dashboards_list_includes_extra_editors_when_resolver_configured(
self,
):
"""List rows get extra_editors too, mirroring the single-object GET."""
admin = self.get_user("admin")
dashboard = self.insert_dashboard(
"extra_editors_list_dashboard",
"extra-editors-list-dashboard",
[admin.id],
)
try:
self.login(ADMIN_USERNAME)
rv = self.client.get("api/v1/dashboard/")
assert rv.status_code == 200
data = json.loads(rv.data.decode("utf-8"))
row = next(
d
for d in data["result"]
if d["dashboard_title"] == dashboard.dashboard_title
)
assert row["extra_editors"] == [123]
finally:
db.session.delete(dashboard)
db.session.commit()
def test_get_charts_admin_sees_existing_charts(self):
"""Regression for #25890: GET /api/v1/chart/ as an Admin user should
return existing charts, not an empty list."""
@@ -993,41 +944,6 @@ class TestDashboardApi(ApiEditorsTestCaseMixin, InsertChartMixin, SupersetTestCa
db.session.delete(chart)
db.session.commit()
def test_get_charts_list_omits_extra_editors_by_default(self):
"""No EXTRA_EDITORS_RESOLVER configured: list rows omit extra_editors."""
admin = self.get_user("admin")
chart = self.insert_chart(
"no_extra_editors_list_chart", [admin.id], 1, params="{}"
)
try:
self.login(ADMIN_USERNAME)
rv = self.client.get("api/v1/chart/")
assert rv.status_code == 200
data = json.loads(rv.data.decode("utf-8"))
row = next(c for c in data["result"] if c["slice_name"] == chart.slice_name)
assert "extra_editors" not in row
finally:
db.session.delete(chart)
db.session.commit()
@with_config({"EXTRA_EDITORS_RESOLVER": lambda resource: [123]})
def test_get_charts_list_includes_extra_editors_when_resolver_configured(self):
"""List rows get extra_editors too, mirroring the single-object GET."""
admin = self.get_user("admin")
chart = self.insert_chart(
"extra_editors_list_chart", [admin.id], 1, params="{}"
)
try:
self.login(ADMIN_USERNAME)
rv = self.client.get("api/v1/chart/")
assert rv.status_code == 200
data = json.loads(rv.data.decode("utf-8"))
row = next(c for c in data["result"] if c["slice_name"] == chart.slice_name)
assert row["extra_editors"] == [123]
finally:
db.session.delete(chart)
db.session.commit()
def test_get_dashboards_filter(self):
"""
Dashboard API: Test get dashboards filter