mirror of
https://github.com/apache/superset.git
synced 2026-08-02 03:52:27 +00:00
Compare commits
1 Commits
master
...
fix-sql-la
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d5e8ad785 |
54
.github/workflows/label-merge-conflicts.yml
vendored
54
.github/workflows/label-merge-conflicts.yml
vendored
@@ -1,54 +0,0 @@
|
||||
name: Label Merge Conflicts
|
||||
|
||||
# Sweeps every open PR and labels the ones GitHub reports as CONFLICTING with
|
||||
# `requires:rebase` (removing it once a rebase makes the PR mergeable again),
|
||||
# so the label can be used to filter the PR backlog for the ones that need a
|
||||
# rebase before they can be reviewed/merged.
|
||||
#
|
||||
# The action itself always re-checks *every* open PR via GraphQL on each run
|
||||
# regardless of what triggered it (see eps1lon/actions-label-merge-conflict's
|
||||
# sources/main.ts) - there's no way to scope it to "just this PR". The
|
||||
# project's own README suggests triggering on `push` (to the default branch)
|
||||
# plus `pull_request_target: [synchronize]`, but on a repo with Superset's PR
|
||||
# volume that combination would re-sweep the entire open-PR list on every
|
||||
# merge to master *and* every push to *any* open PR - many times an hour.
|
||||
# A schedule bounds that to a fixed, predictable cadence instead; adjust it
|
||||
# if 2 hours turns out to be too slow or too chatty in practice.
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 */2 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
# Avoid two full backlog sweeps racing (a manual workflow_dispatch landing
|
||||
# mid-schedule-tick, say); queue rather than cancel so an in-progress
|
||||
# paginated sweep always runs to completion.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
label-merge-conflicts:
|
||||
# Scheduled/dispatch workflows still run on forks that carry this file;
|
||||
# skip anywhere but the canonical repo.
|
||||
if: github.repository == 'apache/superset'
|
||||
name: Label Merge Conflicts
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write # to add/remove requires:rebase and need:merge
|
||||
steps:
|
||||
# ASF Infra allowlists this whole action via a wildcard
|
||||
# (eps1lon/actions-label-merge-conflict@*), so any pinned SHA/version
|
||||
# is already fine here - no Infra ticket needed for future bumps.
|
||||
- uses: eps1lon/actions-label-merge-conflict@0273be72a0bbd58fcd71d0d6c02c209b50d1e5e1 # v3.1.0
|
||||
with:
|
||||
dirtyLabel: "requires:rebase"
|
||||
# A conflicting PR isn't actually ready to merge; strip that signal
|
||||
# if it was previously set so reviewers don't act on a stale one.
|
||||
removeOnDirtyLabel: "need:merge"
|
||||
repoToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Intentionally no commentOnDirty/commentOnClean: the label alone is
|
||||
# the signal (matches the label's existing description, and avoids
|
||||
# a one-time comment storm across the whole backlog on first run).
|
||||
@@ -216,12 +216,6 @@ test('should render the error', async () => {
|
||||
.spyOn(SupersetClient, 'post')
|
||||
.mockRejectedValue(new Error('Something went wrong'));
|
||||
await waitForRender();
|
||||
// The error is wrapped in an Alert component with a stable headline and the
|
||||
// raw error text in the description — no more bare ``<pre>`` elements.
|
||||
expect(await screen.findByRole('alert')).toBeVisible();
|
||||
expect(
|
||||
await screen.findByText('Failed to load drill-to-detail rows'),
|
||||
).toBeVisible();
|
||||
expect(screen.getByText('Error: Something went wrong')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ import BooleanCell from '@superset-ui/core/components/Table/cell-renderers/Boole
|
||||
import NullCell from '@superset-ui/core/components/Table/cell-renderers/NullCell';
|
||||
import TimeCell from '@superset-ui/core/components/Table/cell-renderers/TimeCell';
|
||||
import { EmptyState, Loading } from '@superset-ui/core/components';
|
||||
import { Alert } from '@apache-superset/core/components';
|
||||
import { getDatasourceSamples } from 'src/components/Chart/chartAction';
|
||||
import Table, {
|
||||
ColumnsType,
|
||||
@@ -363,18 +362,13 @@ export default function DrillDetailPane({
|
||||
if (responseError) {
|
||||
// Render error if page download failed
|
||||
tableContent = (
|
||||
<div
|
||||
<pre
|
||||
css={css`
|
||||
margin-top: ${theme.sizeUnit * 4}px;
|
||||
`}
|
||||
>
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message={t('Failed to load drill-to-detail rows')}
|
||||
description={responseError}
|
||||
/>
|
||||
</div>
|
||||
{responseError}
|
||||
</pre>
|
||||
);
|
||||
} else if (bootstrapping) {
|
||||
// Render loading if first page hasn't loaded
|
||||
|
||||
@@ -50,7 +50,6 @@ const DISABLED_REASONS = {
|
||||
DATABASE: t(
|
||||
'Drill to detail is disabled for this database. Change the database settings to enable it.',
|
||||
),
|
||||
DATASOURCE: t('Drill to detail is not available for this datasource type.'),
|
||||
NO_AGGREGATIONS: t(
|
||||
'Drill to detail is disabled because this chart does not group data by dimension value.',
|
||||
),
|
||||
@@ -117,17 +116,6 @@ export const useDrillDetailMenuItems = ({
|
||||
datasources[formData.datasource]?.database?.disable_drill_to_detail,
|
||||
);
|
||||
|
||||
// Capability flag on the datasource itself. Datasources that don't model
|
||||
// raw rows (e.g. semantic views) opt out via ``supports_drill_to_detail``
|
||||
// in the explore data payload.
|
||||
const datasourceSupportsDrillToDetail = useSelector<
|
||||
RootState,
|
||||
boolean | undefined
|
||||
>(
|
||||
({ datasources }) =>
|
||||
datasources[formData.datasource]?.supports_drill_to_detail,
|
||||
);
|
||||
|
||||
const openModal = useCallback(
|
||||
(filters: BinaryQueryObjectFilterClause[], event: MouseEvent) => {
|
||||
onClick(event);
|
||||
@@ -170,10 +158,7 @@ export const useDrillDetailMenuItems = ({
|
||||
|
||||
let drillDisabled;
|
||||
let drillByDisabled;
|
||||
if (datasourceSupportsDrillToDetail === false) {
|
||||
drillDisabled = DISABLED_REASONS.DATASOURCE;
|
||||
drillByDisabled = DISABLED_REASONS.DATASOURCE;
|
||||
} else if (drillToDetailDisabled) {
|
||||
if (drillToDetailDisabled) {
|
||||
drillDisabled = DISABLED_REASONS.DATABASE;
|
||||
drillByDisabled = DISABLED_REASONS.DATABASE;
|
||||
} else if (handlesDimensionContextMenu) {
|
||||
|
||||
@@ -444,45 +444,3 @@ test('context menu renders <NULL> for null dimension values', async () => {
|
||||
await expectDrillToDetailByEnabled();
|
||||
await expectDrillToDetailByDimension(filterNull);
|
||||
});
|
||||
|
||||
const buildStateWithUnsupportedDatasource = () => {
|
||||
const baseState = getMockStoreWithNativeFilters().getState();
|
||||
const datasourceKey = defaultFormData.datasource as string;
|
||||
return {
|
||||
...baseState,
|
||||
datasources: {
|
||||
...baseState.datasources,
|
||||
[datasourceKey]: {
|
||||
...baseState.datasources[datasourceKey],
|
||||
supports_drill_to_detail: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
test('dropdown menu when datasource opts out via supports_drill_to_detail=false', async () => {
|
||||
cleanup();
|
||||
render(<MockRenderChart formData={defaultFormData} />, {
|
||||
useRouter: true,
|
||||
useRedux: true,
|
||||
initialState: buildStateWithUnsupportedDatasource(),
|
||||
});
|
||||
|
||||
await expectDrillToDetailDisabled(
|
||||
'Drill to detail is not available for this datasource type.',
|
||||
);
|
||||
await expectNoDrillToDetailBy();
|
||||
});
|
||||
|
||||
test('context menu when datasource opts out via supports_drill_to_detail=false', async () => {
|
||||
cleanup();
|
||||
render(<MockRenderChart formData={defaultFormData} isContextMenu />, {
|
||||
useRouter: true,
|
||||
useRedux: true,
|
||||
initialState: buildStateWithUnsupportedDatasource(),
|
||||
});
|
||||
|
||||
const message = 'Drill to detail is not available for this datasource type.';
|
||||
await expectDrillToDetailDisabled(message);
|
||||
await expectDrillToDetailByDisabled(message);
|
||||
});
|
||||
|
||||
@@ -566,31 +566,3 @@ test('should pass filterState from dataMask to ChartContainer', () => {
|
||||
mockFilterState,
|
||||
);
|
||||
});
|
||||
|
||||
test('should pass chartStackTrace to ChartContainer so dashboard chart errors stay expandable', () => {
|
||||
// Regression guard for #31858: the dashboard chart wrapper stopped forwarding
|
||||
// the stack trace, so failed charts rendered a flat error with no "See more"
|
||||
// affordance while the same error in Explore stayed expandable.
|
||||
const stackTrace = 'Traceback (most recent call last): ValueError: boom';
|
||||
|
||||
setup(
|
||||
{},
|
||||
{
|
||||
...defaultState,
|
||||
charts: {
|
||||
...defaultState.charts,
|
||||
[queryId]: {
|
||||
...defaultState.charts[queryId],
|
||||
chartStatus: 'failed',
|
||||
chartAlert: 'Something went wrong',
|
||||
chartStackTrace: stackTrace,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(capturedChartContainerProps).toHaveProperty(
|
||||
'chartStackTrace',
|
||||
stackTrace,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -789,7 +789,6 @@ const Chart = (props: ChartProps) => {
|
||||
chartAlert={chart.chartAlert ?? undefined}
|
||||
chartId={props.id}
|
||||
chartStatus={chartStatus ?? undefined}
|
||||
chartStackTrace={chart.chartStackTrace ?? undefined}
|
||||
datasource={datasource}
|
||||
dashboardId={props.dashboardId}
|
||||
initialValues={EMPTY_OBJECT}
|
||||
|
||||
@@ -232,10 +232,6 @@ export type Datasource = Dataset & {
|
||||
// Populated by the dashboard datasets API alongside ``type``; declared here
|
||||
// so callers can rely on structural typing instead of casting.
|
||||
datasource_type?: DatasourceType;
|
||||
/** False when the datasource can't return row samples (e.g. semantic views). */
|
||||
supports_samples?: boolean;
|
||||
/** False when the datasource can't answer drill-to-detail requests. */
|
||||
supports_drill_to_detail?: boolean;
|
||||
};
|
||||
export type DatasourcesState = {
|
||||
[key: string]: Datasource;
|
||||
|
||||
@@ -241,44 +241,25 @@ export const DataTablesPane = ({
|
||||
}
|
||||
}, [resultsTabFallback]);
|
||||
|
||||
// Hide the Samples tab for datasources that don't expose raw rows
|
||||
// (e.g. semantic views). The check is intentionally ``=== false`` so that
|
||||
// datasources from older backends that don't send the flag still show the
|
||||
// tab and preserve current behavior.
|
||||
const showSamplesTab = datasource?.supports_samples !== false;
|
||||
|
||||
// If the datasource swaps to one that doesn't support samples while the
|
||||
// Samples tab is active (e.g. the user picks a semantic view), the tab
|
||||
// disappears from ``tabItems`` and ``activeTabKey`` is orphaned. Fall back
|
||||
// to Results so the panel keeps rendering content.
|
||||
useEffect(() => {
|
||||
if (!showSamplesTab && activeTabKey === ResultTypes.Samples) {
|
||||
setActiveTabKey(ResultTypes.Results);
|
||||
}
|
||||
}, [showSamplesTab, activeTabKey]);
|
||||
const tabItems = [
|
||||
...queryResultsPanes,
|
||||
...(showSamplesTab
|
||||
? [
|
||||
{
|
||||
key: ResultTypes.Samples,
|
||||
label: t('Samples'),
|
||||
children: (
|
||||
<StyledDiv>
|
||||
<SamplesPane
|
||||
datasource={datasource}
|
||||
queryFormData={queryFormData}
|
||||
queryForce={queryForce}
|
||||
isRequest={isRequest.samples}
|
||||
setForceQuery={setForceQuery}
|
||||
isVisible={ResultTypes.Samples === activeTabKey}
|
||||
canDownload={canDownload}
|
||||
/>
|
||||
</StyledDiv>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: ResultTypes.Samples,
|
||||
label: t('Samples'),
|
||||
children: (
|
||||
<StyledDiv>
|
||||
<SamplesPane
|
||||
datasource={datasource}
|
||||
queryFormData={queryFormData}
|
||||
queryForce={queryForce}
|
||||
isRequest={isRequest.samples}
|
||||
setForceQuery={setForceQuery}
|
||||
isVisible={ResultTypes.Samples === activeTabKey}
|
||||
canDownload={canDownload}
|
||||
/>
|
||||
</StyledDiv>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -21,7 +21,6 @@ import { t } from '@apache-superset/core/translation';
|
||||
import { ensureIsArray } from '@superset-ui/core';
|
||||
import { datasetLabelLower } from 'src/features/semanticLayers/label';
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
import { Alert } from '@apache-superset/core/components';
|
||||
import { EmptyState, Loading } from '@superset-ui/core/components';
|
||||
import { GenericDataType } from '@apache-superset/core/common';
|
||||
import { GridTable } from 'src/components/GridTable';
|
||||
@@ -36,7 +35,7 @@ import {
|
||||
import { TableControls, ROW_LIMIT_OPTIONS } from './DataTableControls';
|
||||
import { SamplesPaneProps } from '../types';
|
||||
|
||||
const ErrorAlertWrapper = styled.div`
|
||||
const Error = styled.pre`
|
||||
margin-top: ${({ theme }) => `${theme.sizeUnit * 4}px`};
|
||||
`;
|
||||
|
||||
@@ -156,14 +155,7 @@ export const SamplesPane = ({
|
||||
rowLimitOptions={ROW_LIMIT_OPTIONS}
|
||||
onRowLimitChange={handleRowLimitChange}
|
||||
/>
|
||||
<ErrorAlertWrapper>
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message={t('Failed to load samples')}
|
||||
description={responseError}
|
||||
/>
|
||||
</ErrorAlertWrapper>
|
||||
<Error>{responseError}</Error>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,14 +27,13 @@ import {
|
||||
QueryData,
|
||||
} from '@superset-ui/core';
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
import { Alert } from '@apache-superset/core/components';
|
||||
import { EmptyState, Loading } from '@superset-ui/core/components';
|
||||
import { getChartDataRequest } from 'src/components/Chart/chartAction';
|
||||
import { ResultsPaneProps, QueryResultInterface } from '../types';
|
||||
import { SingleQueryResultPane } from './SingleQueryResultPane';
|
||||
import { TableControls, ROW_LIMIT_OPTIONS } from './DataTableControls';
|
||||
|
||||
const ErrorAlertWrapper = styled.div`
|
||||
const Error = styled.pre`
|
||||
margin-top: ${({ theme }) => `${theme.sizeUnit * 4}px`};
|
||||
`;
|
||||
|
||||
@@ -200,14 +199,7 @@ export const useResultsPane = ({
|
||||
isLoading={false}
|
||||
canDownload={canDownload}
|
||||
/>
|
||||
<ErrorAlertWrapper>
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message={t('Failed to load results')}
|
||||
description={responseError}
|
||||
/>
|
||||
</ErrorAlertWrapper>
|
||||
<Error>{responseError}</Error>
|
||||
</>
|
||||
);
|
||||
return Array(queryCount).fill(err);
|
||||
|
||||
@@ -19,12 +19,7 @@
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { FeatureFlag } from '@superset-ui/core';
|
||||
import * as copyUtils from 'src/utils/copy';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { render, screen, userEvent } from 'spec/helpers/testing-library';
|
||||
import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
|
||||
import { DataTablesPane } from '..';
|
||||
@@ -94,48 +89,6 @@ describe('DataTablesPane', () => {
|
||||
expect(await screen.findByLabelText('Collapse data panel')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Hides Samples tab when datasource opts out via supports_samples=false', async () => {
|
||||
const props = createDataTablesPaneProps(0);
|
||||
const propsWithoutSamples = {
|
||||
...props,
|
||||
datasource: { ...props.datasource, supports_samples: false },
|
||||
};
|
||||
render(<DataTablesPane {...propsWithoutSamples} />, { useRedux: true });
|
||||
expect(await screen.findByText('Results')).toBeVisible();
|
||||
expect(screen.queryByText('Samples')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Falls back to Results when active Samples tab disappears mid-session', async () => {
|
||||
// Regression for codeant Major finding on PR #41509: a datasource swap
|
||||
// that hides the Samples tab while it was the active tab used to leave
|
||||
// ``activeTabKey === 'samples'`` orphaned, rendering a blank panel.
|
||||
const props = createDataTablesPaneProps(0);
|
||||
const { rerender } = render(<DataTablesPane {...props} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
|
||||
// Open the panel and pick the Samples tab.
|
||||
userEvent.click(screen.getByLabelText('Expand data panel'));
|
||||
userEvent.click(await screen.findByText('Samples'));
|
||||
expect(await screen.findByLabelText('Collapse data panel')).toBeVisible();
|
||||
|
||||
// Swap to a datasource that doesn't support samples (e.g. a semantic
|
||||
// view). The Samples tab should disappear and the panel should land on
|
||||
// Results with content still rendered.
|
||||
rerender(
|
||||
<DataTablesPane
|
||||
{...props}
|
||||
datasource={{ ...props.datasource, supports_samples: false }}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Samples')).not.toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Results')).toBeVisible();
|
||||
// Panel stays expanded and renders Results content rather than going blank.
|
||||
expect(screen.getByLabelText('Collapse data panel')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Should copy data table content correctly', async () => {
|
||||
fetchMock.post(
|
||||
'glob:*/api/v1/chart/data?form_data=%7B%22slice_id%22%3A456%7D',
|
||||
|
||||
@@ -84,14 +84,10 @@ describe('SamplesPane', () => {
|
||||
const props = createSamplesPaneProps({
|
||||
datasourceId: 36,
|
||||
});
|
||||
const { findByText, findByRole } = render(<SamplesPane {...props} />, {
|
||||
const { findByText } = render(<SamplesPane {...props} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
|
||||
// The error is now rendered inside an Alert component, with a clear
|
||||
// headline message and the raw error text as the description.
|
||||
expect(await findByRole('alert')).toBeVisible();
|
||||
expect(await findByText('Failed to load samples')).toBeVisible();
|
||||
expect(await findByText('Error: Bad request')).toBeVisible();
|
||||
});
|
||||
|
||||
|
||||
@@ -74,18 +74,6 @@ export type Datasource = Dataset & {
|
||||
schema?: string;
|
||||
is_sqllab_view?: boolean;
|
||||
extra?: string | object;
|
||||
/**
|
||||
* False when the datasource (e.g. a semantic view) doesn't model raw rows
|
||||
* and therefore can't return a row sample. Defaults to true on the server
|
||||
* side; missing here means the explore UI keeps current behavior.
|
||||
*/
|
||||
supports_samples?: boolean;
|
||||
/**
|
||||
* False when the datasource doesn't model raw rows and therefore can't
|
||||
* answer a drill-to-detail query. Tracked separately from
|
||||
* ``supports_samples`` so the two capabilities can diverge.
|
||||
*/
|
||||
supports_drill_to_detail?: boolean;
|
||||
};
|
||||
|
||||
export interface ExplorePageInitialData {
|
||||
|
||||
@@ -235,15 +235,6 @@ def _get_drill_detail(
|
||||
# todo(yongjie): Remove this function,
|
||||
# when determining whether samples should be applied to the time filter.
|
||||
datasource = _get_datasource(query_context, query_obj)
|
||||
# Refuse for datasource types that don't model raw rows (e.g. semantic
|
||||
# views). Mirrors the ``supports_samples`` gate on the ``/samples``
|
||||
# endpoint so drill-detail is hard-blocked on the backend, not just
|
||||
# hidden in the frontend menu. Defaults to ``True`` for any datasource
|
||||
# class that doesn't explicitly opt out.
|
||||
if not getattr(datasource, "supports_drill_to_detail", True):
|
||||
raise QueryObjectValidationError(
|
||||
_("Drill to detail is not available for this datasource type.")
|
||||
)
|
||||
query_obj = copy.copy(query_obj)
|
||||
query_obj.is_timeseries = False
|
||||
query_obj.metrics = None
|
||||
|
||||
@@ -193,16 +193,6 @@ class BaseDatasource(
|
||||
# Only some datasources support Row Level Security
|
||||
is_rls_supported: bool = False
|
||||
|
||||
# Datasources that can return raw row samples (anything backed by a SQL
|
||||
# table can; semantic-layer abstractions cannot, since they only expose
|
||||
# pre-defined metrics and dimensions).
|
||||
supports_samples: bool = True
|
||||
|
||||
# Datasources that can answer "drill to detail" requests — i.e. fetch the
|
||||
# raw rows underlying a chart cell. Conceptually similar to ``samples``
|
||||
# but kept as a separate capability so the two can diverge.
|
||||
supports_drill_to_detail: bool = True
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
# can be a Column or a property pointing to one
|
||||
@@ -496,8 +486,6 @@ class BaseDatasource(
|
||||
"order_by_choices": self.order_by_choices,
|
||||
"verbose_map": self.verbose_map,
|
||||
"select_star": self.select_star,
|
||||
"supports_samples": self.supports_samples,
|
||||
"supports_drill_to_detail": self.supports_drill_to_detail,
|
||||
}
|
||||
|
||||
def data_for_slices( # pylint: disable=too-many-locals # noqa: C901
|
||||
|
||||
@@ -113,9 +113,6 @@ class PinotEngineSpec(BaseEngineSpec):
|
||||
) -> str:
|
||||
# Pinot driver infers TIMESTAMP column as LONG, so make the quick fix.
|
||||
# When the Pinot driver fix this bug, current method could be removed.
|
||||
#
|
||||
# TODO: remove this override once startreedata/pinot-dbapi#224 is
|
||||
# merged and released, and pinotdb is bumped past that version.
|
||||
if isinstance(sqla_column_type, types.TIMESTAMP):
|
||||
return sqla_column_type.compile().upper()
|
||||
|
||||
|
||||
@@ -200,12 +200,6 @@ class SemanticView(AuditMixinNullable, Model):
|
||||
|
||||
__tablename__ = "semantic_views"
|
||||
|
||||
# Semantic views expose pre-defined metrics and dimensions, not raw rows,
|
||||
# so neither the "Samples" tab in Explore nor the "Drill to detail"
|
||||
# affordance from the chart 3-dots menu can return anything meaningful.
|
||||
supports_samples: bool = False
|
||||
supports_drill_to_detail: bool = False
|
||||
|
||||
# Use integer as the primary key for cross-database auto-increment
|
||||
# compatibility (sa.Identity() is not supported in MySQL or SQLite).
|
||||
# The uuid column is a secondary unique identifier used in URLs and perms.
|
||||
@@ -431,8 +425,6 @@ class SemanticView(AuditMixinNullable, Model):
|
||||
"sql": None,
|
||||
"select_star": None,
|
||||
"editors": [],
|
||||
"supports_samples": self.supports_samples,
|
||||
"supports_drill_to_detail": self.supports_drill_to_detail,
|
||||
"description": self.description,
|
||||
"table_name": self.name,
|
||||
"column_types": [
|
||||
|
||||
@@ -161,6 +161,9 @@ def get_query(query_id: int) -> Query:
|
||||
try:
|
||||
return db.session.query(Query).filter_by(id=query_id).one()
|
||||
except Exception as ex:
|
||||
# roll back so a poisoned session (e.g. PendingRollbackError after a
|
||||
# failed flush) doesn't fail every subsequent backoff retry identically
|
||||
db.session.rollback()
|
||||
raise SqlLabException("Failed at getting query") from ex
|
||||
|
||||
|
||||
|
||||
@@ -346,11 +346,6 @@ class ExplorableData(TypedDict, total=False):
|
||||
always_filter_main_dttm: bool
|
||||
normalize_columns: bool
|
||||
rls_filters: list[dict[str, Any]]
|
||||
# Set by datasources that cannot return raw row samples (e.g. semantic
|
||||
# views, which only expose pre-defined metrics and dimensions).
|
||||
supports_samples: bool
|
||||
# Set by datasources that cannot answer drill-to-detail requests.
|
||||
supports_drill_to_detail: bool
|
||||
|
||||
|
||||
VizData: TypeAlias = list[Any] | dict[Any, Any] | None
|
||||
|
||||
@@ -223,7 +223,7 @@ msgstr "% du total"
|
||||
|
||||
#, python-format
|
||||
msgid "%(alertType)s \"%(alertName)s\" triggered successfully"
|
||||
msgstr "%(alertType)s « %(alertName)s » déclenché avec succès"
|
||||
msgstr ""
|
||||
|
||||
#, python-format
|
||||
msgid "%(dialect)s cannot be used as a data source for security reasons."
|
||||
@@ -1390,10 +1390,10 @@ msgstr ""
|
||||
" ou négative par rapport à la valeur de comparaison."
|
||||
|
||||
msgid "Adhoc metric SQL expression is invalid"
|
||||
msgstr "L'expression SQL de la mesure ad hoc est invalide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Adhoc metric aggregate is invalid"
|
||||
msgstr "L'agrégat de la mesure ad hoc est invalide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Adjust how this database will interact with SQL Lab."
|
||||
msgstr "Ajuster la façon dont cette base de données interagira avec SQL Lab."
|
||||
@@ -2018,8 +2018,6 @@ msgid ""
|
||||
"Angle at which the first slice begins, in degrees. 90° starts at the top,"
|
||||
" 0°/360° at the right, 270° at the bottom, and 180° at the left."
|
||||
msgstr ""
|
||||
"Angle auquel commence la première part, en degrés. 90° démarre en haut, "
|
||||
"0°/360° à droite, 270° en bas et 180° à gauche."
|
||||
|
||||
msgid "Angle at which to end progress axis"
|
||||
msgstr "Angle de fin de l'axe de progression"
|
||||
@@ -2280,7 +2278,7 @@ msgid "Are you sure you want to delete the selected layers?"
|
||||
msgstr "Voulez-vous vraiment supprimer les couches sélectionnées?"
|
||||
|
||||
msgid "Are you sure you want to delete the selected queries?"
|
||||
msgstr "Voulez-vous vraiment supprimer les requêtes sélectionnées ?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to delete the selected roles?"
|
||||
msgstr "Voulez-vous vraiment supprimer les rôles sélectionnés ?"
|
||||
@@ -3529,7 +3527,7 @@ msgid "Clear local theme"
|
||||
msgstr "Supprimer le thème local"
|
||||
|
||||
msgid "Clear search"
|
||||
msgstr "Effacer la recherche"
|
||||
msgstr ""
|
||||
|
||||
msgid "Clear the selection to revert to the system default theme"
|
||||
msgstr "Effacer la sélection pour revenir au thème par défaut du système"
|
||||
@@ -4058,7 +4056,7 @@ msgid "Connection failed, please check your connection settings."
|
||||
msgstr "La connexion a échoué, veuillez vérifier vos paramètres de connexion"
|
||||
|
||||
msgid "Connection looks good!"
|
||||
msgstr "La connexion fonctionne !"
|
||||
msgstr ""
|
||||
|
||||
msgid "Contains"
|
||||
msgstr "Contient"
|
||||
@@ -4146,7 +4144,7 @@ msgid "Copy query"
|
||||
msgstr "Copier la requête"
|
||||
|
||||
msgid "Copy query URL"
|
||||
msgstr "Copier l'URL de la requête"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy query link to your clipboard"
|
||||
msgstr "Copier le lien de la requête vers le presse-papier"
|
||||
@@ -4276,8 +4274,6 @@ msgid ""
|
||||
"Create a new tag and assign it to existing entities like charts or "
|
||||
"dashboards"
|
||||
msgstr ""
|
||||
"Créer une balise et l'affecter à des entités existantes comme des "
|
||||
"graphiques ou des tableaux de bord"
|
||||
|
||||
msgid "Create and explore dataset"
|
||||
msgstr "Créer et explorer un jeu de données"
|
||||
@@ -4306,7 +4302,7 @@ msgstr "Créé par"
|
||||
msgid "Created by me"
|
||||
msgstr "Créé par moi"
|
||||
|
||||
#, python-format
|
||||
, python-format
|
||||
msgid "Created by: %s"
|
||||
msgstr "Créé par : %s"
|
||||
|
||||
@@ -4601,9 +4597,6 @@ msgid ""
|
||||
"Dashboard cannot be restored because its slug is now used by another "
|
||||
"active dashboard. Rename one of the dashboards and retry."
|
||||
msgstr ""
|
||||
"Le tableau de bord ne peut pas être restauré car son slug est désormais "
|
||||
"utilisé par un autre tableau de bord actif. Renommez l'un des deux "
|
||||
"tableaux de bord et réessayez."
|
||||
|
||||
msgid "Dashboard cannot be unfavorited."
|
||||
msgstr "Le tableau de bord n'a pas pu être retiré des favoris."
|
||||
@@ -5290,7 +5283,7 @@ msgid "Delete item"
|
||||
msgstr "Supprimer l'élément"
|
||||
|
||||
msgid "Delete query"
|
||||
msgstr "Supprimer la requête"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete role"
|
||||
msgstr "Supprimer le rôle"
|
||||
@@ -5602,9 +5595,6 @@ msgid ""
|
||||
"Display charts on a map. For using this plugin, users first have to "
|
||||
"create any other chart that can then be placed on the map."
|
||||
msgstr ""
|
||||
"Affiche des graphiques sur une carte. Pour utiliser ce module, il faut "
|
||||
"d'abord créer un autre graphique, qui pourra ensuite être placé sur la "
|
||||
"carte."
|
||||
|
||||
msgid "Display column in the chart"
|
||||
msgstr "Afficher la colonne dans le graphique"
|
||||
@@ -6000,7 +5990,7 @@ msgstr "ERREUR"
|
||||
|
||||
#, python-format
|
||||
msgid "ERROR: %s"
|
||||
msgstr "ERREUR : %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edge length"
|
||||
msgstr "Longueur du bord"
|
||||
@@ -6092,7 +6082,7 @@ msgid "Edit properties"
|
||||
msgstr "Modifier les propriétés"
|
||||
|
||||
msgid "Edit query"
|
||||
msgstr "Modifier la requête"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit report"
|
||||
msgstr "Modifier le rapport"
|
||||
@@ -6182,7 +6172,7 @@ msgid "Email link"
|
||||
msgstr "Lien par courriel"
|
||||
|
||||
msgid "Email recipients"
|
||||
msgstr "Destinataires du courriel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Email reports active"
|
||||
msgstr "Rapports par courriel actifs"
|
||||
@@ -6435,7 +6425,7 @@ msgid "Entity"
|
||||
msgstr "Entité"
|
||||
|
||||
msgid "Entries per page"
|
||||
msgstr "Entrées par page"
|
||||
msgstr ""
|
||||
|
||||
msgid "Equal Date Sizes"
|
||||
msgstr "Taille des dates égales"
|
||||
@@ -6703,7 +6693,7 @@ msgid "Export as Example"
|
||||
msgstr "Exporter comme exemple"
|
||||
|
||||
msgid "Export as PDF"
|
||||
msgstr "Exporter en PDF"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export cancelled"
|
||||
msgstr "Export annulé"
|
||||
@@ -6722,7 +6712,7 @@ msgid "Export failed: %s"
|
||||
msgstr "Export échoué : %s"
|
||||
|
||||
msgid "Export query"
|
||||
msgstr "Exporter la requête"
|
||||
msgstr ""
|
||||
|
||||
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: cs, de, ja,
|
||||
# lv, ro, ru, sk, sr, sr_Latn, tr, uk]
|
||||
@@ -6730,7 +6720,7 @@ msgid "Export screenshot (jpeg)"
|
||||
msgstr "Exporter la capture d'écran (jpeg)"
|
||||
|
||||
msgid "Export screenshot (png)"
|
||||
msgstr "Exporter la capture d'écran (png)"
|
||||
msgstr ""
|
||||
|
||||
#, python-format
|
||||
msgid "Export successful: %s"
|
||||
@@ -6774,15 +6764,11 @@ msgid ""
|
||||
"Exporting semantic views is not supported yet — %s semantic-view row(s) "
|
||||
"were skipped."
|
||||
msgstr ""
|
||||
"L'export des vues sémantiques n'est pas encore pris en charge — %s "
|
||||
"ligne(s) de vue sémantique ont été ignorées."
|
||||
|
||||
msgid ""
|
||||
"Exporting semantic views is not supported yet. Deselect the semantic-view"
|
||||
" rows and try again."
|
||||
msgstr ""
|
||||
"L'export des vues sémantiques n'est pas encore pris en charge. "
|
||||
"Désélectionnez les lignes de vue sémantique et réessayez."
|
||||
|
||||
msgid "Expose database in SQL Lab"
|
||||
msgstr "Exposer la base de données dans SQL Lab"
|
||||
@@ -6895,8 +6881,6 @@ msgid ""
|
||||
"Failed to export chart data. Please try again or contact your "
|
||||
"administrator."
|
||||
msgstr ""
|
||||
"Échec de l'export des données du graphique. Réessayez ou contactez votre "
|
||||
"administrateur."
|
||||
|
||||
msgid "Failed to fetch API keys"
|
||||
msgstr "Tout Dé-Sélectionner"
|
||||
@@ -6972,7 +6956,7 @@ msgstr "Échec du marquage des éléments"
|
||||
|
||||
#, python-format
|
||||
msgid "Failed to trigger %(alertType)s \"%(alertName)s\": %(error)s"
|
||||
msgstr "Échec du déclenchement de %(alertType)s « %(alertName)s » : %(error)s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to update report"
|
||||
msgstr "Échec de la mise à jour du rapport"
|
||||
@@ -7331,7 +7315,7 @@ msgid "Forecast periods"
|
||||
msgstr "Périodes de prévision"
|
||||
|
||||
msgid "Forecast requires at least 2 data points"
|
||||
msgstr "La prévision nécessite au moins 2 points de données"
|
||||
msgstr ""
|
||||
|
||||
msgid "Foreign key"
|
||||
msgstr "Clé étrangère"
|
||||
@@ -7387,10 +7371,10 @@ msgstr ""
|
||||
" sont présentes, le formatage revient aux nombres neutres."
|
||||
|
||||
msgid "Formatted CSV attached in email"
|
||||
msgstr "Fichier CSV mis en forme joint au courriel"
|
||||
msgstr "CSV formatté attaché dans le courriel"
|
||||
|
||||
msgid "Formatted Excel attached in email"
|
||||
msgstr "Fichier Excel mis en forme joint au courriel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Formatted date"
|
||||
msgstr "Date formatée"
|
||||
@@ -8334,7 +8318,7 @@ msgid "Label for your query"
|
||||
msgstr "Label pour votre requête"
|
||||
|
||||
msgid "Label must not be empty."
|
||||
msgstr "L'étiquette ne doit pas être vide."
|
||||
msgstr ""
|
||||
|
||||
msgid "Label position"
|
||||
msgstr "Position de l'étiquette"
|
||||
@@ -8635,7 +8619,7 @@ msgid "Lines encoding"
|
||||
msgstr "Codage des lignes"
|
||||
|
||||
msgid "Link Copied!"
|
||||
msgstr "Lien copié !"
|
||||
msgstr ""
|
||||
|
||||
msgid "List"
|
||||
msgstr "Liste"
|
||||
@@ -9572,7 +9556,7 @@ msgstr ""
|
||||
"enregistrement temporel"
|
||||
|
||||
msgid "No data found"
|
||||
msgstr "Aucune donnée trouvée"
|
||||
msgstr ""
|
||||
|
||||
msgid "No data in file"
|
||||
msgstr "Pas de données dans le fichier"
|
||||
@@ -9988,7 +9972,7 @@ msgstr "Une ou plusieurs mesures n'existent pas"
|
||||
|
||||
#, python-format
|
||||
msgid "One or more parameters are missing: %(missing)s"
|
||||
msgstr "Un ou plusieurs paramètres sont manquants : %(missing)s"
|
||||
msgstr ""
|
||||
|
||||
msgid "One or more parameters needed to configure a database are missing."
|
||||
msgstr ""
|
||||
@@ -10691,11 +10675,6 @@ msgid ""
|
||||
"period (e.g. today so far) against complete prior periods (e.g. all of "
|
||||
"yesterday)."
|
||||
msgstr ""
|
||||
"Tracer chaque série décalée dans le temps sur toute sa plage temporelle "
|
||||
"au lieu de la tronquer à celle de la série principale. Utile pour "
|
||||
"comparer une période en cours partielle (par exemple aujourd'hui jusqu'à "
|
||||
"maintenant) à des périodes antérieures complètes (par exemple la journée "
|
||||
"d'hier entière)."
|
||||
|
||||
msgid "Plot the distance (like flight paths) between origin and destination."
|
||||
msgstr ""
|
||||
@@ -11156,12 +11135,6 @@ msgid ""
|
||||
"except the subjects defined in the filter, and can be used to define what"
|
||||
" users can see if no RLS filters within a filter group apply to them."
|
||||
msgstr ""
|
||||
"Les filtres classiques ajoutent des clauses WHERE aux requêtes lorsqu'un "
|
||||
"utilisateur correspond à un sujet référencé par le filtre. Les filtres de"
|
||||
" base appliquent des filtres à toutes les requêtes sauf pour les sujets "
|
||||
"définis dans le filtre ; ils permettent de définir ce que voient les "
|
||||
"utilisateurs auxquels aucun filtre RLS d'un groupe de filtres ne "
|
||||
"s'applique."
|
||||
|
||||
msgid "Relational"
|
||||
msgstr "Relationnel"
|
||||
@@ -11202,7 +11175,7 @@ msgid "Remove customization"
|
||||
msgstr "Supprimer la personnalisation"
|
||||
|
||||
msgid "Remove dependency"
|
||||
msgstr "Supprimer la dépendance"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove filter"
|
||||
msgstr "Supprimer le filtre"
|
||||
@@ -11211,13 +11184,13 @@ msgid "Remove item"
|
||||
msgstr "Supprimer l’élément"
|
||||
|
||||
msgid "Remove notification method"
|
||||
msgstr "Supprimer le mode de notification"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove query from log"
|
||||
msgstr "Supprimer la requête des journaux"
|
||||
|
||||
msgid "Remove sheet"
|
||||
msgstr "Supprimer la feuille"
|
||||
msgstr ""
|
||||
|
||||
#, python-format
|
||||
msgid "Removed 1 column from the virtual dataset"
|
||||
@@ -11261,7 +11234,7 @@ msgid "Report Schedule delete failed."
|
||||
msgstr "La planification de rapport n'a pas être supprimée."
|
||||
|
||||
msgid "Report Schedule execute now failed."
|
||||
msgstr "L'exécution immédiate de la planification de rapport a échoué."
|
||||
msgstr ""
|
||||
|
||||
msgid "Report Schedule execution failed when generating a csv."
|
||||
msgstr ""
|
||||
@@ -11285,8 +11258,6 @@ msgstr ""
|
||||
|
||||
msgid "Report Schedule execution failed when generating an Excel file."
|
||||
msgstr ""
|
||||
"L'exécution de la planification de rapport a échoué lors de la génération"
|
||||
" du fichier Excel."
|
||||
|
||||
msgid "Report Schedule execution got an unexpected error."
|
||||
msgstr ""
|
||||
@@ -11298,15 +11269,10 @@ msgid ""
|
||||
"Please configure a Celery broker (Redis or RabbitMQ) and worker "
|
||||
"processes."
|
||||
msgstr ""
|
||||
"L'exécution de la planification de rapport nécessite un backend Celery "
|
||||
"configuré. Configurez un broker Celery (Redis ou RabbitMQ) et des "
|
||||
"processus worker."
|
||||
|
||||
#, python-format
|
||||
msgid "Report Schedule executor user %(username)s was not found."
|
||||
msgstr ""
|
||||
"L'utilisateur %(username)s exécutant la planification de rapport est "
|
||||
"introuvable."
|
||||
|
||||
msgid "Report Schedule is still working, refusing to re-compute."
|
||||
msgstr ""
|
||||
@@ -12015,7 +11981,7 @@ msgid "Search Metrics & Columns"
|
||||
msgstr "Rechercher les mesures et les colonnes"
|
||||
|
||||
msgid "Search a channel by name, or paste a channel ID"
|
||||
msgstr "Rechercher un canal par son nom, ou coller un identifiant de canal"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search all charts"
|
||||
msgstr "Rechercher tous les graphiques"
|
||||
@@ -12060,7 +12026,7 @@ msgid "Search owners"
|
||||
msgstr "Rechercher des propriétaires"
|
||||
|
||||
msgid "Search records"
|
||||
msgstr "Rechercher des enregistrements"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search roles"
|
||||
msgstr "Recherche de rôles"
|
||||
@@ -12076,8 +12042,6 @@ msgstr "Rechercher…"
|
||||
|
||||
msgid "Searches all text fields: Name, Description, Database & Schema"
|
||||
msgstr ""
|
||||
"Recherche dans tous les champs texte : nom, description, base de données "
|
||||
"et schéma"
|
||||
|
||||
msgid "Second"
|
||||
msgstr "Seconde"
|
||||
@@ -12120,7 +12084,7 @@ msgid "See all %(tableName)s"
|
||||
msgstr "Voir tout %(tableName)s"
|
||||
|
||||
msgid "See all dashboards"
|
||||
msgstr "Voir tous les tableaux de bord"
|
||||
msgstr ""
|
||||
|
||||
msgid "See less"
|
||||
msgstr "Voir moins"
|
||||
@@ -12411,10 +12375,10 @@ msgid "Select operator"
|
||||
msgstr "Sélectionner l'opérateur"
|
||||
|
||||
msgid "Select or type BCC recipients"
|
||||
msgstr "Sélectionner ou saisir les destinataires en Cci"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select or type CC recipients"
|
||||
msgstr "Sélectionner ou saisir les destinataires en Cc"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select or type a custom value..."
|
||||
msgstr "Sélectionner ou renseigner une valeur personnalisé..."
|
||||
@@ -12426,7 +12390,7 @@ msgid "Select or type dataset name"
|
||||
msgstr "Sélectionner la base de données ou taper le nom du jeu de données"
|
||||
|
||||
msgid "Select or type email recipients"
|
||||
msgstr "Sélectionner ou saisir les destinataires du courriel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select page size"
|
||||
msgstr "Sélectionner la taille de la page"
|
||||
@@ -12675,7 +12639,7 @@ msgid "Send as CSV"
|
||||
msgstr "Envoyer comme CSV"
|
||||
|
||||
msgid "Send as Excel"
|
||||
msgstr "Envoyer comme Excel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Send as PDF"
|
||||
msgstr "Envoyer comme PDF"
|
||||
@@ -12907,7 +12871,7 @@ msgid "Show Metric Names"
|
||||
msgstr "Afficher les noms de mesure"
|
||||
|
||||
msgid "Show Null Values"
|
||||
msgstr "Afficher les valeurs nulles"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show Range Filter"
|
||||
msgstr "Afficher l'intervalle de filtre"
|
||||
@@ -12948,17 +12912,13 @@ msgstr ""
|
||||
"autrement min/max dans les données."
|
||||
|
||||
msgid "Show a draggable slider to control the visible range of the Y-axis."
|
||||
msgstr "Afficher un curseur déplaçable pour contrôler la plage visible de l'axe Y."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Show a summary row of total aggregations: the selected metrics in "
|
||||
"aggregate mode, or the sum of numeric columns in raw records mode. Note "
|
||||
"that row limit does not apply to the result."
|
||||
msgstr ""
|
||||
"Afficher une ligne de synthèse des agrégats totaux : les mesures "
|
||||
"sélectionnées en mode agrégé, ou la somme des colonnes numériques en mode"
|
||||
" enregistrements bruts. Notez que la limite de lignes ne s'applique pas "
|
||||
"au résultat."
|
||||
|
||||
msgid "Show all columns"
|
||||
msgstr "Afficher toutes les colonnes"
|
||||
@@ -12999,7 +12959,7 @@ msgid "Show entries per page"
|
||||
msgstr "Afficher le nombre d'éléments par page"
|
||||
|
||||
msgid "Show full range for time shift"
|
||||
msgstr "Afficher la plage complète pour le décalage temporel"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Show hierarchical relationships of data, with the value represented by "
|
||||
@@ -13086,8 +13046,6 @@ msgid ""
|
||||
"Showcases a metric along with a comparison of value, change, and percent "
|
||||
"change for a selected time period."
|
||||
msgstr ""
|
||||
"Met en avant une mesure avec une comparaison de la valeur, de l'écart et "
|
||||
"de l'écart en pourcentage sur une période sélectionnée."
|
||||
|
||||
msgid ""
|
||||
"Showcases a single metric front-and-center. Big number is best used to "
|
||||
@@ -13231,7 +13189,7 @@ msgid "Solid"
|
||||
msgstr "Solide"
|
||||
|
||||
msgid "Solid background"
|
||||
msgstr "Fond uni"
|
||||
msgstr ""
|
||||
|
||||
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr,
|
||||
# sr_Latn]
|
||||
@@ -13255,14 +13213,12 @@ msgstr ""
|
||||
"seront pas effacés"
|
||||
|
||||
msgid "Some tables are not shown. Refine your search."
|
||||
msgstr "Certaines tables ne sont pas affichées. Affinez votre recherche."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Something went wrong loading the dashboard. Check the dev console for "
|
||||
"details."
|
||||
msgstr ""
|
||||
"Un problème est survenu lors du chargement du tableau de bord. Consultez "
|
||||
"la console développeur pour plus de détails."
|
||||
|
||||
msgid "Something went wrong while saving the user info"
|
||||
msgstr "Une erreur s'est produite. Réessayez plus tard."
|
||||
@@ -13672,7 +13628,7 @@ msgid "Success"
|
||||
msgstr "Réussite"
|
||||
|
||||
msgid "Success message"
|
||||
msgstr "Message de succès"
|
||||
msgstr ""
|
||||
|
||||
#, python-format
|
||||
msgid "Successfully changed %s!"
|
||||
@@ -13746,7 +13702,7 @@ msgid "Swap rows and columns"
|
||||
msgstr "Échanger les rangées et les colonnes"
|
||||
|
||||
msgid "Sweep angle"
|
||||
msgstr "Angle de balayage"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Swiss army knife for visualizing data. Choose between step, line, "
|
||||
@@ -13914,7 +13870,7 @@ msgid "Tag created"
|
||||
msgstr "Balise créée"
|
||||
|
||||
msgid "Tag description"
|
||||
msgstr "Description de la balise"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tag name"
|
||||
msgstr "Nom de la balise"
|
||||
@@ -14044,13 +14000,11 @@ msgid "Text align"
|
||||
msgstr "Alignement du texte"
|
||||
|
||||
msgid "Text embedded in email"
|
||||
msgstr "Texte encapsulé dans le courriel"
|
||||
msgstr "Text encapsulé dans le courriel"
|
||||
|
||||
#, python-format
|
||||
msgid "The %(key)s in metadata_cache_timeout must be a non-negative integer."
|
||||
msgstr ""
|
||||
"La valeur de %(key)s dans metadata_cache_timeout doit être un entier "
|
||||
"positif ou nul."
|
||||
|
||||
#, python-format
|
||||
msgid "The %s"
|
||||
@@ -14170,9 +14124,6 @@ msgid ""
|
||||
"The chart data is too large to download. Please try reducing the date "
|
||||
"range, limiting rows, or using fewer columns."
|
||||
msgstr ""
|
||||
"Les données du graphique sont trop volumineuses pour être téléchargées. "
|
||||
"Réduisez la plage de dates, limitez le nombre de lignes ou utilisez moins"
|
||||
" de colonnes."
|
||||
|
||||
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr,
|
||||
# sr_Latn]
|
||||
@@ -14190,7 +14141,7 @@ msgstr ""
|
||||
" ou mettez à jour le rapport pour pointer vers un graphique actif."
|
||||
|
||||
msgid "The chat failed to load."
|
||||
msgstr "La conversation n'a pas pu être chargée."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The classic. Great for showing how much of a company each investor gets, "
|
||||
@@ -14288,8 +14239,6 @@ msgstr ""
|
||||
|
||||
msgid "The dashboard you are looking for may have been deleted or moved."
|
||||
msgstr ""
|
||||
"Le tableau de bord que vous recherchez a peut-être été supprimé ou "
|
||||
"déplacé."
|
||||
|
||||
msgid "The data source seems to have been deleted"
|
||||
msgstr "La source de données semble avoir été effacée"
|
||||
@@ -14574,8 +14523,6 @@ msgid ""
|
||||
"The metadata_cache_timeout must be a mapping from string keys to non-"
|
||||
"negative integer values."
|
||||
msgstr ""
|
||||
"metadata_cache_timeout doit être une correspondance entre des clés de "
|
||||
"type chaîne et des entiers positifs ou nuls."
|
||||
|
||||
#, python-format
|
||||
msgid ""
|
||||
@@ -14812,8 +14759,6 @@ msgstr "Cette requête contient un ou plusieurs paramètres de modèle malformé
|
||||
|
||||
msgid "The query context datasource does not match the chart datasource"
|
||||
msgstr ""
|
||||
"La source de données du contexte de requête ne correspond pas à celle du "
|
||||
"graphique"
|
||||
|
||||
msgid "The query couldn't be loaded"
|
||||
msgstr "La requête ne peut pas être chargée"
|
||||
@@ -15255,7 +15200,7 @@ msgid "There was an error fetching the filtered charts and dashboards:"
|
||||
msgstr "Une erreur s’est produite lors de la récupération des graphiques et tableaux de bord filtrés :"
|
||||
|
||||
msgid "There was an error generating the permalink."
|
||||
msgstr "Une erreur s'est produite lors de la génération du lien permanent."
|
||||
msgstr ""
|
||||
|
||||
msgid "There was an error loading groups."
|
||||
msgstr "Une erreur s'est produite lors du chargement des groupes."
|
||||
@@ -15360,8 +15305,6 @@ msgstr ""
|
||||
#, python-format
|
||||
msgid "There was an issue deleting the selected queries: %s"
|
||||
msgstr ""
|
||||
"Un problème est survenu lors de la suppression des requêtes sélectionnées"
|
||||
" : %s"
|
||||
|
||||
#, python-format
|
||||
msgid "There was an issue deleting the selected templates: %s"
|
||||
@@ -15408,7 +15351,7 @@ msgid "There was an issue exporting the selected dashboards"
|
||||
msgstr "Il y a eu un problème lors de l'export des tableaux de bord sélectionnés"
|
||||
|
||||
msgid "There was an issue exporting the selected queries"
|
||||
msgstr "Un problème est survenu lors de l'export des requêtes sélectionnées"
|
||||
msgstr ""
|
||||
|
||||
msgid "There was an issue exporting the selected themes"
|
||||
msgstr "Il y a eu un problème lors de l'export des thèmes sélectionnés"
|
||||
@@ -15575,7 +15518,7 @@ msgstr ""
|
||||
"être transmis au graphique contenant les données d'annotation."
|
||||
|
||||
msgid "This dashboard does not exist"
|
||||
msgstr "Ce tableau de bord n'existe pas"
|
||||
msgstr ""
|
||||
|
||||
msgid "This dashboard is managed externally, and can't be edited in Superset"
|
||||
msgstr ""
|
||||
@@ -15963,8 +15906,6 @@ msgstr "Fragment de temps"
|
||||
|
||||
msgid "Time Grain must be specified when using Time Comparison."
|
||||
msgstr ""
|
||||
"Le fragment de temps doit être précisé lors de l'utilisation de la "
|
||||
"comparaison de temps."
|
||||
|
||||
msgid "Time Granularity"
|
||||
msgstr "Fragmentation de Temps"
|
||||
@@ -16254,10 +16195,6 @@ msgid ""
|
||||
" angle is a multiple of 90°, the chart is automatically re-centered to "
|
||||
"make use of the empty space."
|
||||
msgstr ""
|
||||
"Angle total couvert par le graphique, en degrés. 360° dessine un cercle "
|
||||
"complet et 180° un demi-anneau. Lorsque le balayage est inférieur ou égal"
|
||||
" à 180° et que l'angle de départ est un multiple de 90°, le graphique est"
|
||||
" automatiquement recentré pour exploiter l'espace vide."
|
||||
|
||||
msgid "Total color"
|
||||
msgstr "Couleur du total"
|
||||
@@ -16282,7 +16219,7 @@ msgid "Transparent"
|
||||
msgstr "Transparent"
|
||||
|
||||
msgid "Transparent background"
|
||||
msgstr "Fond transparent"
|
||||
msgstr ""
|
||||
|
||||
msgid "Transpose pivot"
|
||||
msgstr "Pivot de transposition"
|
||||
@@ -16312,7 +16249,7 @@ msgid "Trigger Alert If..."
|
||||
msgstr "Déclencher une alerte si…"
|
||||
|
||||
msgid "Trigger now"
|
||||
msgstr "Déclencher maintenant"
|
||||
msgstr ""
|
||||
|
||||
msgid "True"
|
||||
msgstr "Est vrai"
|
||||
@@ -16431,7 +16368,7 @@ msgid "URL parameters"
|
||||
msgstr "Paramètres URL"
|
||||
|
||||
msgid "UUID to track the execution status"
|
||||
msgstr "UUID permettant de suivre l'état de l'exécution"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unable to calculate such a date delta"
|
||||
msgstr "Impossible de calculer un delta de date comme celui-ci"
|
||||
@@ -16498,7 +16435,7 @@ msgstr "Impossible de générer les données de téléchargement"
|
||||
|
||||
#, python-format
|
||||
msgid "Unable to generate forecast: %(error)s"
|
||||
msgstr "Impossible de générer la prévision : %(error)s"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Unable to identify temporal column for date range time comparison.Please "
|
||||
@@ -16513,8 +16450,6 @@ msgid ""
|
||||
"Unable to interpret the time offset: %(offset)s. Use a relative time such"
|
||||
" as \"1 month ago\"."
|
||||
msgstr ""
|
||||
"Impossible d'interpréter le décalage temporel : %(offset)s. Utilisez une "
|
||||
"expression relative telle que « 1 month ago »."
|
||||
|
||||
msgid ""
|
||||
"Unable to load columns for the selected table. Please select a different "
|
||||
@@ -17562,8 +17497,6 @@ msgstr "Affichage ou non des bulles au-dessus des pays"
|
||||
|
||||
msgid "Whether to display entries with null values in the hierarchy"
|
||||
msgstr ""
|
||||
"Afficher ou non les entrées dont les valeurs sont nulles dans la "
|
||||
"hiérarchie"
|
||||
|
||||
msgid "Whether to display in the chart"
|
||||
msgstr "Afficher ou non dans le graphique"
|
||||
@@ -17699,9 +17632,6 @@ msgid ""
|
||||
"Whether to sort tooltip by the selected metric in descending order. On "
|
||||
"stacked charts, values are shown in ascending order."
|
||||
msgstr ""
|
||||
"Trier ou non l'infobulle par la mesure sélectionnée dans l'ordre "
|
||||
"décroissant. Sur les graphiques empilés, les valeurs sont affichées dans "
|
||||
"l'ordre croissant."
|
||||
|
||||
msgid "Whether to truncate metrics"
|
||||
msgstr "Tronquer ou non les mesures"
|
||||
@@ -17861,7 +17791,7 @@ msgid "Y-axis bounds"
|
||||
msgstr "Limites de l’axe des ordonnées"
|
||||
|
||||
msgid "Y-axis range slider"
|
||||
msgstr "Curseur de plage de l'axe Y"
|
||||
msgstr ""
|
||||
|
||||
msgid "Y-scale interval"
|
||||
msgstr "Intervalle d'échelle Y"
|
||||
@@ -18136,41 +18066,26 @@ msgid ""
|
||||
"You must be a chart editor in order to delete. Please reach out to a "
|
||||
"chart editor to request modifications or edit access."
|
||||
msgstr ""
|
||||
"Vous devez être éditeur du graphique pour pouvoir supprimer. Contactez un"
|
||||
" éditeur du graphique pour demander des modifications ou un accès en "
|
||||
"modification."
|
||||
|
||||
msgid ""
|
||||
"You must be a chart editor in order to edit. Please reach out to a chart "
|
||||
"editor to request modifications or edit access."
|
||||
msgstr ""
|
||||
"Vous devez être éditeur du graphique pour pouvoir modifier. Contactez un "
|
||||
"éditeur du graphique pour demander des modifications ou un accès en "
|
||||
"modification."
|
||||
|
||||
msgid ""
|
||||
"You must be a dashboard editor in order to delete. Please reach out to a "
|
||||
"dashboard editor to request modifications or edit access."
|
||||
msgstr ""
|
||||
"Vous devez être éditeur du tableau de bord pour pouvoir supprimer. "
|
||||
"Contactez un éditeur du tableau de bord pour demander des modifications "
|
||||
"ou un accès en modification."
|
||||
|
||||
msgid ""
|
||||
"You must be a dashboard editor in order to edit. Please reach out to a "
|
||||
"dashboard editor to request modifications or edit access."
|
||||
msgstr ""
|
||||
"Vous devez être éditeur du tableau de bord pour pouvoir modifier. "
|
||||
"Contactez un éditeur du tableau de bord pour demander des modifications "
|
||||
"ou un accès en modification."
|
||||
|
||||
msgid ""
|
||||
"You must be a dataset editor in order to delete. Please reach out to a "
|
||||
"dataset editor to request modifications or edit access."
|
||||
msgstr ""
|
||||
"Vous devez être éditeur de l'ensemble de données pour pouvoir supprimer. "
|
||||
"Contactez un éditeur de l'ensemble de données pour demander des "
|
||||
"modifications ou un accès en modification."
|
||||
|
||||
msgid ""
|
||||
"You must be a dataset editor in order to edit. Please reach out to a "
|
||||
@@ -18257,11 +18172,6 @@ msgid ""
|
||||
"into multiple dashboards) or raise the "
|
||||
"SUPERSET_DASHBOARD_POSITION_DATA_LIMIT config setting."
|
||||
msgstr ""
|
||||
"Votre tableau de bord est trop volumineux pour être enregistré : la "
|
||||
"longueur sérialisée de la disposition est de %s alors que la limite est "
|
||||
"de %s. Réduisez la taille du tableau de bord (par exemple en le scindant "
|
||||
"en plusieurs tableaux de bord) ou augmentez le paramètre de configuration"
|
||||
" SUPERSET_DASHBOARD_POSITION_DATA_LIMIT."
|
||||
|
||||
msgid "Your query could not be saved"
|
||||
msgstr "Votre requête n'a pas pu être enregistrée"
|
||||
@@ -19116,7 +19026,7 @@ msgid "quarter"
|
||||
msgstr "trimestre"
|
||||
|
||||
msgid "queries"
|
||||
msgstr "requêtes"
|
||||
msgstr ""
|
||||
|
||||
msgid "query"
|
||||
msgstr "requête"
|
||||
@@ -19162,7 +19072,7 @@ msgid "seconds"
|
||||
msgstr "secondes"
|
||||
|
||||
msgid "semantic layer"
|
||||
msgstr "couche sémantique"
|
||||
msgstr ""
|
||||
|
||||
msgid "series"
|
||||
msgstr "série"
|
||||
|
||||
@@ -202,23 +202,6 @@ class Datasource(BaseSupersetView):
|
||||
payload = SamplesPayloadSchema().load(request.json)
|
||||
except ValidationError as err:
|
||||
return json_error_response(err.messages, status=400)
|
||||
|
||||
# Refuse early for datasource types that don't model raw rows
|
||||
# (e.g. semantic views, which only expose pre-defined metrics and
|
||||
# dimensions). Without this gate the request would still go through
|
||||
# the standard query pipeline and fail with an opaque 500.
|
||||
# ``supports_samples`` defaults to True for any datasource class that
|
||||
# doesn't explicitly opt out, so SqlaTable/Query/SavedQuery continue
|
||||
# to work without needing the attribute declared on each class.
|
||||
ds_class = DatasourceDAO.sources.get(
|
||||
DatasourceType(params["datasource_type"]),
|
||||
)
|
||||
if ds_class is not None and not getattr(ds_class, "supports_samples", True):
|
||||
return json_error_response(
|
||||
_("Samples are not available for this datasource type."),
|
||||
status=400,
|
||||
)
|
||||
|
||||
dashboard_id = None
|
||||
if security_manager.is_guest_user():
|
||||
if not params["dashboard_id"]:
|
||||
|
||||
@@ -1,70 +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.
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from superset.common.query_actions import _get_drill_detail
|
||||
from superset.exceptions import QueryObjectValidationError
|
||||
|
||||
|
||||
def test_get_drill_detail_refuses_datasource_that_opts_out() -> None:
|
||||
"""
|
||||
A datasource with ``supports_drill_to_detail = False`` (e.g. semantic
|
||||
views) must be hard-blocked on the server. Without this gate the request
|
||||
would fall through to ``_get_full`` and fail with an opaque error, and
|
||||
the flag would only be enforced by the frontend menu — leaving the
|
||||
chart-data API endpoint accepting drill-detail requests it shouldn't.
|
||||
"""
|
||||
datasource = MagicMock()
|
||||
datasource.supports_drill_to_detail = False
|
||||
|
||||
query_obj = MagicMock()
|
||||
query_obj.datasource = datasource
|
||||
|
||||
query_context = MagicMock()
|
||||
|
||||
with pytest.raises(
|
||||
QueryObjectValidationError,
|
||||
match="Drill to detail is not available",
|
||||
):
|
||||
_get_drill_detail(query_context, query_obj)
|
||||
|
||||
|
||||
def test_get_drill_detail_allows_datasource_without_flag() -> None:
|
||||
"""
|
||||
Datasources that don't declare the flag (e.g. legacy ``SqlaTable``
|
||||
subclasses via ``getattr`` default) must continue to work — the gate
|
||||
only fires when the flag is explicitly ``False``.
|
||||
"""
|
||||
datasource = MagicMock(spec=["columns"])
|
||||
column = MagicMock()
|
||||
column.column_name = "id"
|
||||
datasource.columns = [column]
|
||||
|
||||
query_obj = MagicMock()
|
||||
query_obj.datasource = datasource
|
||||
query_obj.columns = []
|
||||
|
||||
query_context = MagicMock()
|
||||
|
||||
expected_payload: dict[str, list[dict[str, str]]] = {"data": []}
|
||||
with patch(
|
||||
"superset.common.query_actions._get_full", return_value=expected_payload
|
||||
) as mock_get_full:
|
||||
assert _get_drill_detail(query_context, query_obj) is expected_payload
|
||||
mock_get_full.assert_called_once()
|
||||
@@ -653,15 +653,6 @@ def test_semantic_view_data(
|
||||
assert data["table_name"] == "Orders View"
|
||||
assert data["datasource_name"] == "Orders View"
|
||||
assert data["offset"] == 0
|
||||
# Semantic views don't model raw rows, so neither samples nor
|
||||
# drill-to-detail are available.
|
||||
assert data["supports_samples"] is False
|
||||
assert data["supports_drill_to_detail"] is False
|
||||
|
||||
|
||||
def test_semantic_view_supports_samples_is_false() -> None:
|
||||
"""The class-level flag opts SemanticView out of the Samples affordance."""
|
||||
assert SemanticView.supports_samples is False
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -776,11 +767,6 @@ def test_semantic_view_data_populates_time_grain_sqla(
|
||||
assert grain_durations == sorted(["PT1H", "P1D", "P1M"])
|
||||
|
||||
|
||||
def test_semantic_view_supports_drill_to_detail_is_false() -> None:
|
||||
"""The class-level flag opts SemanticView out of Drill to detail."""
|
||||
assert SemanticView.supports_drill_to_detail is False
|
||||
|
||||
|
||||
def test_semantic_view_get_query_result(
|
||||
mock_implementation: MagicMock,
|
||||
) -> None:
|
||||
|
||||
@@ -35,6 +35,7 @@ from superset.sql.parse import SQLStatement, Table
|
||||
from superset.sql_lab import (
|
||||
execute_query,
|
||||
execute_sql_statements,
|
||||
get_query,
|
||||
get_sql_results,
|
||||
)
|
||||
from superset.utils.rls import apply_rls, get_predicates_for_table
|
||||
@@ -71,6 +72,34 @@ def test_execute_query(mocker: MockerFixture, app: None) -> None:
|
||||
SupersetResultSet.assert_called_with([(42,)], cursor.description, db_engine_spec)
|
||||
|
||||
|
||||
def test_get_query_rolls_back_session_before_retrying(
|
||||
mocker: MockerFixture, app: SupersetApp
|
||||
) -> None:
|
||||
"""
|
||||
A broken transaction (e.g. `PendingRollbackError` following a failed flush)
|
||||
leaves the session unusable until `session.rollback()` is called, so without
|
||||
it every `backoff` retry would reuse the same poisoned session and fail
|
||||
identically. `get_query` must roll back on failure so each retry gets a
|
||||
clean session and has a real chance to succeed.
|
||||
"""
|
||||
# avoid actually sleeping through the `backoff` decorator's retry interval
|
||||
mocker.patch("backoff._sync.time.sleep")
|
||||
|
||||
expected_query = mocker.MagicMock()
|
||||
mock_one = mocker.patch("superset.sql_lab.db.session.query")
|
||||
mock_one.return_value.filter_by.return_value.one.side_effect = [
|
||||
Exception("session is broken"),
|
||||
expected_query,
|
||||
]
|
||||
mock_rollback = mocker.patch("superset.sql_lab.db.session.rollback")
|
||||
|
||||
result = get_query(query_id=1)
|
||||
|
||||
assert result is expected_query
|
||||
assert mock_one.return_value.filter_by.return_value.one.call_count == 2
|
||||
mock_rollback.assert_called_once()
|
||||
|
||||
|
||||
@with_config(
|
||||
{
|
||||
"SQLLAB_PAYLOAD_MAX_MB": 50,
|
||||
|
||||
@@ -310,66 +310,3 @@ def test_save_non_editor_with_editors_field_is_rejected(
|
||||
raw_save(_view_self())
|
||||
|
||||
mock_security_manager.raise_for_editorship.assert_called_once_with(mock_orm)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Datasource.samples
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@patch("superset.views.datasource.views._", lambda s: s)
|
||||
@patch("superset.views.datasource.views.get_samples")
|
||||
@patch("superset.views.datasource.views.json_error_response")
|
||||
@patch("superset.views.datasource.views.security_manager", new_callable=MagicMock)
|
||||
def test_samples_returns_400_for_unsupported_datasource_type(
|
||||
mock_security_manager: MagicMock,
|
||||
mock_json_error_response: MagicMock,
|
||||
mock_get_samples: MagicMock,
|
||||
) -> None:
|
||||
"""Semantic views can't return raw samples — endpoint should refuse with 400."""
|
||||
from flask import Flask
|
||||
|
||||
mock_security_manager.is_guest_user.return_value = False
|
||||
mock_json_error_response.return_value = "error-response"
|
||||
|
||||
raw_samples = _get_view_func("samples")
|
||||
app = Flask(__name__)
|
||||
with app.test_request_context(
|
||||
"/datasource/samples?datasource_type=semantic_view&datasource_id=1",
|
||||
method="POST",
|
||||
json={},
|
||||
):
|
||||
result = raw_samples(_view_self())
|
||||
|
||||
assert result == "error-response"
|
||||
mock_json_error_response.assert_called_once()
|
||||
_, kwargs = mock_json_error_response.call_args
|
||||
assert kwargs.get("status") == 400
|
||||
# The bail-out must happen before any sample fetching is attempted.
|
||||
mock_get_samples.assert_not_called()
|
||||
|
||||
|
||||
@patch("superset.views.datasource.views.get_samples")
|
||||
@patch("superset.views.datasource.views.security_manager", new_callable=MagicMock)
|
||||
def test_samples_proceeds_for_supported_datasource_type(
|
||||
mock_security_manager: MagicMock,
|
||||
mock_get_samples: MagicMock,
|
||||
) -> None:
|
||||
"""A `query` datasource (supports_samples=True) bypasses the 400 short-circuit."""
|
||||
from flask import Flask
|
||||
|
||||
mock_security_manager.is_guest_user.return_value = False
|
||||
mock_get_samples.return_value = {"rows": []}
|
||||
|
||||
view = _view_self()
|
||||
raw_samples = _get_view_func("samples")
|
||||
app = Flask(__name__)
|
||||
with app.test_request_context(
|
||||
"/datasource/samples?datasource_type=query&datasource_id=1",
|
||||
method="POST",
|
||||
json={},
|
||||
):
|
||||
raw_samples(view)
|
||||
|
||||
mock_get_samples.assert_called_once()
|
||||
view.json_response.assert_called_once_with({"result": {"rows": []}})
|
||||
|
||||
Reference in New Issue
Block a user