mirror of
https://github.com/apache/superset.git
synced 2026-06-24 00:49:17 +00:00
Compare commits
5 Commits
fix-query-
...
issue-3607
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59ddd52789 | ||
|
|
60f29ba6fb | ||
|
|
306f4c14cf | ||
|
|
310dcd7b94 | ||
|
|
008c7c6517 |
@@ -67,6 +67,7 @@ export function normalizeTimeColumn(
|
||||
sqlExpression: formData.x_axis,
|
||||
label: formData.x_axis,
|
||||
expressionType: 'SQL',
|
||||
isColumnReference: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface AdhocColumn {
|
||||
optionName?: string;
|
||||
sqlExpression: string;
|
||||
expressionType: 'SQL';
|
||||
isColumnReference?: boolean;
|
||||
columnType?: 'BASE_AXIS' | 'SERIES';
|
||||
timeGrain?: string;
|
||||
datasourceWarning?: boolean;
|
||||
@@ -74,6 +75,10 @@ export function isAdhocColumn(column?: any): column is AdhocColumn {
|
||||
);
|
||||
}
|
||||
|
||||
export function isAdhocColumnReference(column?: any): column is AdhocColumn {
|
||||
return isAdhocColumn(column) && column?.isColumnReference === true;
|
||||
}
|
||||
|
||||
export function isQueryFormColumn(column: any): column is QueryFormColumn {
|
||||
return isPhysicalColumn(column) || isAdhocColumn(column);
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ test('should support different columns for x-axis and granularity', () => {
|
||||
{
|
||||
timeGrain: 'P1Y',
|
||||
columnType: 'BASE_AXIS',
|
||||
isColumnReference: true,
|
||||
sqlExpression: 'time_column_in_x_axis',
|
||||
label: 'time_column_in_x_axis',
|
||||
expressionType: 'SQL',
|
||||
|
||||
@@ -101,36 +101,35 @@ describe('queryObject conversion', () => {
|
||||
|
||||
it('should convert queryObject', () => {
|
||||
const { queries } = buildQuery({ ...formData, x_axis: 'time_column' });
|
||||
expect(queries[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
granularity: 'time_column',
|
||||
time_range: '1 year ago : 2013',
|
||||
extras: { having: '', where: '', time_grain_sqla: 'P1Y' },
|
||||
columns: [
|
||||
{
|
||||
columnType: 'BASE_AXIS',
|
||||
expressionType: 'SQL',
|
||||
label: 'time_column',
|
||||
sqlExpression: 'time_column',
|
||||
timeGrain: 'P1Y',
|
||||
expect(queries[0]).toMatchObject({
|
||||
granularity: 'time_column',
|
||||
time_range: '1 year ago : 2013',
|
||||
extras: { having: '', where: '', time_grain_sqla: 'P1Y' },
|
||||
columns: [
|
||||
{
|
||||
columnType: 'BASE_AXIS',
|
||||
expressionType: 'SQL',
|
||||
label: 'time_column',
|
||||
sqlExpression: 'time_column',
|
||||
timeGrain: 'P1Y',
|
||||
isColumnReference: true,
|
||||
},
|
||||
'col1',
|
||||
],
|
||||
series_columns: ['col1'],
|
||||
metrics: ['count(*)'],
|
||||
post_processing: [
|
||||
{
|
||||
operation: 'pivot',
|
||||
options: {
|
||||
aggregates: { 'count(*)': { operator: 'mean' } },
|
||||
columns: ['col1'],
|
||||
drop_missing_columns: true,
|
||||
index: ['time_column'],
|
||||
},
|
||||
'col1',
|
||||
],
|
||||
series_columns: ['col1'],
|
||||
metrics: ['count(*)'],
|
||||
post_processing: [
|
||||
{
|
||||
operation: 'pivot',
|
||||
options: {
|
||||
aggregates: { 'count(*)': { operator: 'mean' } },
|
||||
columns: ['col1'],
|
||||
drop_missing_columns: true,
|
||||
index: ['time_column'],
|
||||
},
|
||||
},
|
||||
{ operation: 'flatten' },
|
||||
],
|
||||
}),
|
||||
);
|
||||
},
|
||||
{ operation: 'flatten' },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -139,6 +139,31 @@ function cellWidth({
|
||||
return perc2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a column identifier for use in HTML id attributes and CSS selectors.
|
||||
* Replaces characters that are invalid in CSS selectors with safe alternatives.
|
||||
*
|
||||
* Note: The returned value should be prefixed with a string (e.g., "header-")
|
||||
* to ensure it forms a valid HTML ID (IDs cannot start with a digit).
|
||||
*
|
||||
* Exported for testing.
|
||||
*/
|
||||
export function sanitizeHeaderId(columnId: string): string {
|
||||
return (
|
||||
columnId
|
||||
// Semantic replacements first: preserve meaning in IDs for readability
|
||||
// (e.g., '%pct_nice' → 'percentpct_nice' instead of '_pct_nice')
|
||||
.replace(/%/g, 'percent')
|
||||
.replace(/#/g, 'hash')
|
||||
.replace(/△/g, 'delta')
|
||||
// Generic sanitization for remaining special characters
|
||||
.replace(/\s+/g, '_')
|
||||
.replace(/[^a-zA-Z0-9_-]/g, '_')
|
||||
.replace(/_+/g, '_') // Collapse consecutive underscores
|
||||
.replace(/^_+|_+$/g, '') // Trim leading/trailing underscores
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cell left margin (offset) calculation for horizontal bar chart elements
|
||||
* when alignPositiveNegative is not set
|
||||
@@ -844,6 +869,9 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
}
|
||||
}
|
||||
|
||||
// Cache sanitized header ID to avoid recomputing it multiple times
|
||||
const headerId = sanitizeHeaderId(column.originalLabel ?? column.key);
|
||||
|
||||
return {
|
||||
id: String(i), // to allow duplicate column keys
|
||||
// must use custom accessor to allow `.` in column names
|
||||
@@ -969,7 +997,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
}
|
||||
|
||||
const cellProps = {
|
||||
'aria-labelledby': `header-${column.key}`,
|
||||
'aria-labelledby': `header-${headerId}`,
|
||||
role: 'cell',
|
||||
// show raw number in title in case of numeric values
|
||||
title: typeof value === 'number' ? String(value) : undefined,
|
||||
@@ -1056,7 +1084,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
},
|
||||
Header: ({ column: col, onClick, style, onDragStart, onDrop }) => (
|
||||
<th
|
||||
id={`header-${column.originalLabel}`}
|
||||
id={`header-${headerId}`}
|
||||
title={t('Shift + Click to sort by multiple columns')}
|
||||
className={[className, col.isSorted ? 'is-sorted' : ''].join(' ')}
|
||||
style={{
|
||||
|
||||
@@ -18,15 +18,93 @@
|
||||
*/
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from '@superset-ui/core/spec';
|
||||
import TableChart from '../src/TableChart';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import TableChart, { sanitizeHeaderId } from '../src/TableChart';
|
||||
import transformProps from '../src/transformProps';
|
||||
import DateWithFormatter from '../src/utils/DateWithFormatter';
|
||||
import testData from './testData';
|
||||
import { ProviderWrapper } from './testHelpers';
|
||||
|
||||
test('sanitizeHeaderId should sanitize percent sign', () => {
|
||||
expect(sanitizeHeaderId('%pct_nice')).toBe('percentpct_nice');
|
||||
});
|
||||
|
||||
test('sanitizeHeaderId should sanitize hash/pound sign', () => {
|
||||
expect(sanitizeHeaderId('# metric_1')).toBe('hash_metric_1');
|
||||
});
|
||||
|
||||
test('sanitizeHeaderId should sanitize delta symbol', () => {
|
||||
expect(sanitizeHeaderId('△ delta')).toBe('delta_delta');
|
||||
});
|
||||
|
||||
test('sanitizeHeaderId should replace spaces with underscores', () => {
|
||||
expect(sanitizeHeaderId('Main metric_1')).toBe('Main_metric_1');
|
||||
expect(sanitizeHeaderId('multiple spaces')).toBe('multiple_spaces');
|
||||
});
|
||||
|
||||
test('sanitizeHeaderId should handle multiple special characters', () => {
|
||||
expect(sanitizeHeaderId('% #△ test')).toBe('percent_hashdelta_test');
|
||||
expect(sanitizeHeaderId('% # △ test')).toBe('percent_hash_delta_test');
|
||||
});
|
||||
|
||||
test('sanitizeHeaderId should preserve alphanumeric, underscore, and hyphen', () => {
|
||||
expect(sanitizeHeaderId('valid-name_123')).toBe('valid-name_123');
|
||||
});
|
||||
|
||||
test('sanitizeHeaderId should replace other special characters with underscore', () => {
|
||||
expect(sanitizeHeaderId('col@name!test')).toBe('col_name_test');
|
||||
expect(sanitizeHeaderId('test.column')).toBe('test_column');
|
||||
});
|
||||
|
||||
test('sanitizeHeaderId should handle edge cases', () => {
|
||||
expect(sanitizeHeaderId('')).toBe('');
|
||||
expect(sanitizeHeaderId('simple')).toBe('simple');
|
||||
});
|
||||
|
||||
test('sanitizeHeaderId should collapse consecutive underscores', () => {
|
||||
expect(sanitizeHeaderId('test @@ space')).toBe('test_space');
|
||||
expect(sanitizeHeaderId('col___name')).toBe('col_name');
|
||||
expect(sanitizeHeaderId('a b c')).toBe('a_b_c');
|
||||
expect(sanitizeHeaderId('test@@name')).toBe('test_name');
|
||||
});
|
||||
|
||||
test('sanitizeHeaderId should remove leading underscores', () => {
|
||||
expect(sanitizeHeaderId('@col')).toBe('col');
|
||||
expect(sanitizeHeaderId('!revenue')).toBe('revenue');
|
||||
expect(sanitizeHeaderId('@@test')).toBe('test');
|
||||
expect(sanitizeHeaderId(' leading_spaces')).toBe('leading_spaces');
|
||||
});
|
||||
|
||||
test('sanitizeHeaderId should remove trailing underscores', () => {
|
||||
expect(sanitizeHeaderId('col@')).toBe('col');
|
||||
expect(sanitizeHeaderId('revenue!')).toBe('revenue');
|
||||
expect(sanitizeHeaderId('test@@')).toBe('test');
|
||||
expect(sanitizeHeaderId('trailing_spaces ')).toBe('trailing_spaces');
|
||||
});
|
||||
|
||||
test('sanitizeHeaderId should remove leading and trailing underscores', () => {
|
||||
expect(sanitizeHeaderId('@col@')).toBe('col');
|
||||
expect(sanitizeHeaderId('!test!')).toBe('test');
|
||||
expect(sanitizeHeaderId(' spaced ')).toBe('spaced');
|
||||
expect(sanitizeHeaderId('@@multiple@@')).toBe('multiple');
|
||||
});
|
||||
|
||||
test('sanitizeHeaderId should handle inputs with only special characters', () => {
|
||||
expect(sanitizeHeaderId('@')).toBe('');
|
||||
expect(sanitizeHeaderId('@@')).toBe('');
|
||||
expect(sanitizeHeaderId(' ')).toBe('');
|
||||
expect(sanitizeHeaderId('!@$')).toBe('');
|
||||
expect(sanitizeHeaderId('!@#$')).toBe('hash'); // # is replaced with 'hash' (semantic replacement)
|
||||
// Semantic replacements produce readable output even when alone
|
||||
expect(sanitizeHeaderId('%')).toBe('percent');
|
||||
expect(sanitizeHeaderId('#')).toBe('hash');
|
||||
expect(sanitizeHeaderId('△')).toBe('delta');
|
||||
expect(sanitizeHeaderId('% # △')).toBe('percent_hash_delta');
|
||||
});
|
||||
|
||||
describe('plugin-chart-table', () => {
|
||||
describe('transformProps', () => {
|
||||
it('should parse pageLength to pageSize', () => {
|
||||
test('should parse pageLength to pageSize', () => {
|
||||
expect(transformProps(testData.basic).pageSize).toBe(20);
|
||||
expect(
|
||||
transformProps({
|
||||
@@ -42,13 +120,13 @@ describe('plugin-chart-table', () => {
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it('should memoize data records', () => {
|
||||
test('should memoize data records', () => {
|
||||
expect(transformProps(testData.basic).data).toBe(
|
||||
transformProps(testData.basic).data,
|
||||
);
|
||||
});
|
||||
|
||||
it('should memoize columns meta', () => {
|
||||
test('should memoize columns meta', () => {
|
||||
expect(transformProps(testData.basic).columns).toBe(
|
||||
transformProps({
|
||||
...testData.basic,
|
||||
@@ -57,14 +135,14 @@ describe('plugin-chart-table', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should format timestamp', () => {
|
||||
test('should format timestamp', () => {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const parsedDate = transformProps(testData.basic).data[0]
|
||||
.__timestamp as DateWithFormatter;
|
||||
expect(String(parsedDate)).toBe('2020-01-01 12:34:56');
|
||||
expect(parsedDate.getTime()).toBe(1577882096000);
|
||||
});
|
||||
it('should process comparison columns when time_compare and comparison_type are set', () => {
|
||||
test('should process comparison columns when time_compare and comparison_type are set', () => {
|
||||
const transformedProps = transformProps(testData.comparison);
|
||||
const comparisonColumns = transformedProps.columns.filter(
|
||||
col =>
|
||||
@@ -86,7 +164,7 @@ describe('plugin-chart-table', () => {
|
||||
expect(comparisonColumns.some(col => col.label === '%')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not process comparison columns when time_compare is empty', () => {
|
||||
test('should not process comparison columns when time_compare is empty', () => {
|
||||
const propsWithoutTimeCompare = {
|
||||
...testData.comparison,
|
||||
rawFormData: {
|
||||
@@ -109,7 +187,7 @@ describe('plugin-chart-table', () => {
|
||||
expect(comparisonColumns.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should correctly apply column configuration for comparison columns', () => {
|
||||
test('should correctly apply column configuration for comparison columns', () => {
|
||||
const transformedProps = transformProps(testData.comparisonWithConfig);
|
||||
|
||||
const comparisonColumns = transformedProps.columns.filter(
|
||||
@@ -147,7 +225,7 @@ describe('plugin-chart-table', () => {
|
||||
expect(percentMetricConfig?.config).toEqual({ d3NumberFormat: '.3f' });
|
||||
});
|
||||
|
||||
it('should correctly format comparison columns using getComparisonColFormatter', () => {
|
||||
test('should correctly format comparison columns using getComparisonColFormatter', () => {
|
||||
const transformedProps = transformProps(testData.comparisonWithConfig);
|
||||
const comparisonColumns = transformedProps.columns.filter(
|
||||
col =>
|
||||
@@ -178,7 +256,7 @@ describe('plugin-chart-table', () => {
|
||||
expect(formattedPercentMetric).toBe('0.123');
|
||||
});
|
||||
|
||||
it('should set originalLabel for comparison columns when time_compare and comparison_type are set', () => {
|
||||
test('should set originalLabel for comparison columns when time_compare and comparison_type are set', () => {
|
||||
const transformedProps = transformProps(testData.comparison);
|
||||
|
||||
// Check if comparison columns are processed
|
||||
@@ -265,7 +343,7 @@ describe('plugin-chart-table', () => {
|
||||
});
|
||||
|
||||
describe('TableChart', () => {
|
||||
it('render basic data', () => {
|
||||
test('render basic data', () => {
|
||||
render(
|
||||
<TableChart {...transformProps(testData.basic)} sticky={false} />,
|
||||
);
|
||||
@@ -284,12 +362,9 @@ describe('plugin-chart-table', () => {
|
||||
expect(cells[8]).toHaveTextContent('N/A');
|
||||
});
|
||||
|
||||
it('render advanced data', () => {
|
||||
test('render advanced data', () => {
|
||||
render(
|
||||
<>
|
||||
<TableChart {...transformProps(testData.advanced)} sticky={false} />
|
||||
,
|
||||
</>,
|
||||
<TableChart {...transformProps(testData.advanced)} sticky={false} />,
|
||||
);
|
||||
const secondColumnHeader = screen.getByText('Sum of Num');
|
||||
expect(secondColumnHeader).toBeInTheDocument();
|
||||
@@ -304,7 +379,7 @@ describe('plugin-chart-table', () => {
|
||||
expect(cells[4]).toHaveTextContent('2.47k');
|
||||
});
|
||||
|
||||
it('render advanced data with currencies', () => {
|
||||
test('render advanced data with currencies', () => {
|
||||
render(
|
||||
ProviderWrapper({
|
||||
children: (
|
||||
@@ -324,7 +399,7 @@ describe('plugin-chart-table', () => {
|
||||
expect(cells[4]).toHaveTextContent('$ 2.47k');
|
||||
});
|
||||
|
||||
it('render data with a bigint value in a raw record mode', () => {
|
||||
test('render data with a bigint value in a raw record mode', () => {
|
||||
render(
|
||||
ProviderWrapper({
|
||||
children: (
|
||||
@@ -345,7 +420,7 @@ describe('plugin-chart-table', () => {
|
||||
expect(cells[3]).toHaveTextContent('1234567890123456789');
|
||||
});
|
||||
|
||||
it('render raw data', () => {
|
||||
test('render raw data', () => {
|
||||
const props = transformProps({
|
||||
...testData.raw,
|
||||
rawFormData: { ...testData.raw.rawFormData },
|
||||
@@ -362,7 +437,7 @@ describe('plugin-chart-table', () => {
|
||||
expect(cells[1]).toHaveTextContent('0');
|
||||
});
|
||||
|
||||
it('render raw data with currencies', () => {
|
||||
test('render raw data with currencies', () => {
|
||||
const props = transformProps({
|
||||
...testData.raw,
|
||||
rawFormData: {
|
||||
@@ -387,7 +462,7 @@ describe('plugin-chart-table', () => {
|
||||
expect(cells[2]).toHaveTextContent('$ 0');
|
||||
});
|
||||
|
||||
it('render small formatted data with currencies', () => {
|
||||
test('render small formatted data with currencies', () => {
|
||||
const props = transformProps({
|
||||
...testData.raw,
|
||||
rawFormData: {
|
||||
@@ -429,14 +504,14 @@ describe('plugin-chart-table', () => {
|
||||
expect(cells[2]).toHaveTextContent('$ 0.61');
|
||||
});
|
||||
|
||||
it('render empty data', () => {
|
||||
test('render empty data', () => {
|
||||
render(
|
||||
<TableChart {...transformProps(testData.empty)} sticky={false} />,
|
||||
);
|
||||
expect(screen.getByText('No records found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('render color with column color formatter', () => {
|
||||
test('render color with column color formatter', () => {
|
||||
render(
|
||||
ProviderWrapper({
|
||||
children: (
|
||||
@@ -466,8 +541,8 @@ describe('plugin-chart-table', () => {
|
||||
expect(getComputedStyle(screen.getByTitle('2467')).background).toBe('');
|
||||
});
|
||||
|
||||
it('render cell without color', () => {
|
||||
const dataWithEmptyCell = testData.advanced.queriesData[0];
|
||||
test('render cell without color', () => {
|
||||
const dataWithEmptyCell = cloneDeep(testData.advanced.queriesData[0]);
|
||||
dataWithEmptyCell.data.push({
|
||||
__timestamp: null,
|
||||
name: 'Noah',
|
||||
@@ -507,7 +582,7 @@ describe('plugin-chart-table', () => {
|
||||
);
|
||||
expect(getComputedStyle(screen.getByText('N/A')).background).toBe('');
|
||||
});
|
||||
it('should display original label in grouped headers', () => {
|
||||
test('should display original label in grouped headers', () => {
|
||||
const props = transformProps(testData.comparison);
|
||||
|
||||
render(<TableChart {...props} sticky={false} />);
|
||||
@@ -522,7 +597,142 @@ describe('plugin-chart-table', () => {
|
||||
expect(hasMetricHeaders).toBe(true);
|
||||
});
|
||||
|
||||
it('render cell bars properly, and only when it is toggled on in both regular and percent metrics', () => {
|
||||
test('should set meaningful header IDs for time-comparison columns', () => {
|
||||
// Test time-comparison columns have proper IDs
|
||||
// Uses originalLabel (e.g., "metric_1") which is sanitized for CSS safety
|
||||
const props = transformProps(testData.comparison);
|
||||
|
||||
const { container } = render(<TableChart {...props} sticky={false} />);
|
||||
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
|
||||
// All headers should have IDs
|
||||
const headersWithIds = headers.filter(header => header.id);
|
||||
expect(headersWithIds.length).toBeGreaterThan(0);
|
||||
|
||||
// None should have "header-undefined"
|
||||
const undefinedHeaders = headersWithIds.filter(header =>
|
||||
header.id.includes('undefined'),
|
||||
);
|
||||
expect(undefinedHeaders).toHaveLength(0);
|
||||
|
||||
// Should have IDs based on sanitized originalLabel (e.g., "metric_1")
|
||||
const hasMetricHeaders = headersWithIds.some(
|
||||
header =>
|
||||
header.id.includes('metric_1') || header.id.includes('metric_2'),
|
||||
);
|
||||
expect(hasMetricHeaders).toBe(true);
|
||||
|
||||
// CRITICAL: Verify sanitization - no spaces or special chars in any header ID
|
||||
headersWithIds.forEach(header => {
|
||||
// IDs must not contain spaces (would break CSS selectors and ARIA)
|
||||
expect(header.id).not.toMatch(/\s/);
|
||||
// IDs must not contain special chars like %, #, △
|
||||
expect(header.id).not.toMatch(/[%#△]/);
|
||||
// IDs should only contain valid characters: alphanumeric, underscore, hyphen
|
||||
expect(header.id).toMatch(/^header-[a-zA-Z0-9_-]+$/);
|
||||
});
|
||||
|
||||
// CRITICAL: Verify ALL cells reference valid headers (no broken ARIA)
|
||||
const cellsWithLabels = container.querySelectorAll(
|
||||
'td[aria-labelledby]',
|
||||
);
|
||||
cellsWithLabels.forEach(cell => {
|
||||
const labelledBy = cell.getAttribute('aria-labelledby');
|
||||
if (labelledBy) {
|
||||
// Check that the ID doesn't contain spaces (would be interpreted as multiple IDs)
|
||||
expect(labelledBy).not.toMatch(/\s/);
|
||||
// Check that the ID doesn't contain special characters
|
||||
expect(labelledBy).not.toMatch(/[%#△]/);
|
||||
// Verify the referenced header actually exists
|
||||
const referencedHeader = container.querySelector(
|
||||
`#${CSS.escape(labelledBy)}`,
|
||||
);
|
||||
expect(referencedHeader).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('should set meaningful header IDs for regular table columns', () => {
|
||||
// Test regular (non-time-comparison) columns have proper IDs
|
||||
// Uses fallback to column.key since originalLabel is undefined
|
||||
const props = transformProps(testData.advanced);
|
||||
|
||||
const { container } = render(
|
||||
ProviderWrapper({
|
||||
children: <TableChart {...props} sticky={false} />,
|
||||
}),
|
||||
);
|
||||
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
|
||||
// Test 1: "name" column (regular string column)
|
||||
const nameHeader = headers.find(header =>
|
||||
header.textContent?.includes('name'),
|
||||
);
|
||||
expect(nameHeader).toBeDefined();
|
||||
expect(nameHeader?.id).toBe('header-name'); // Falls back to column.key
|
||||
|
||||
// Verify cells reference this header correctly
|
||||
const nameCells = container.querySelectorAll(
|
||||
'td[aria-labelledby="header-name"]',
|
||||
);
|
||||
expect(nameCells.length).toBeGreaterThan(0);
|
||||
|
||||
// Test 2: "sum__num" column (metric with verbose map "Sum of Num")
|
||||
const sumHeader = headers.find(header =>
|
||||
header.textContent?.includes('Sum of Num'),
|
||||
);
|
||||
expect(sumHeader).toBeDefined();
|
||||
expect(sumHeader?.id).toBe('header-sum_num'); // Falls back to column.key, consecutive underscores collapsed
|
||||
|
||||
// Verify cells reference this header correctly
|
||||
const sumCells = container.querySelectorAll(
|
||||
'td[aria-labelledby="header-sum_num"]',
|
||||
);
|
||||
expect(sumCells.length).toBeGreaterThan(0);
|
||||
|
||||
// Test 3: Verify NO headers have "undefined" in their ID
|
||||
const undefinedHeaders = headers.filter(header =>
|
||||
header.id?.includes('undefined'),
|
||||
);
|
||||
expect(undefinedHeaders).toHaveLength(0);
|
||||
|
||||
// Test 4: Verify ALL headers have proper IDs (no missing IDs)
|
||||
const headersWithIds = headers.filter(header => header.id);
|
||||
expect(headersWithIds.length).toBe(headers.length);
|
||||
|
||||
// Test 5: Verify ALL header IDs are properly sanitized
|
||||
headersWithIds.forEach(header => {
|
||||
// IDs must not contain spaces
|
||||
expect(header.id).not.toMatch(/\s/);
|
||||
// IDs must not contain special chars like % (from %pct_nice column)
|
||||
expect(header.id).not.toMatch(/[%#△]/);
|
||||
// IDs should only contain valid CSS selector characters
|
||||
expect(header.id).toMatch(/^header-[a-zA-Z0-9_-]+$/);
|
||||
});
|
||||
|
||||
// Test 6: Verify ALL cells reference valid headers (no broken ARIA)
|
||||
const cellsWithLabels = container.querySelectorAll(
|
||||
'td[aria-labelledby]',
|
||||
);
|
||||
cellsWithLabels.forEach(cell => {
|
||||
const labelledBy = cell.getAttribute('aria-labelledby');
|
||||
if (labelledBy) {
|
||||
// Verify no spaces (would be interpreted as multiple IDs)
|
||||
expect(labelledBy).not.toMatch(/\s/);
|
||||
// Verify no special characters
|
||||
expect(labelledBy).not.toMatch(/[%#△]/);
|
||||
// Verify the referenced header actually exists
|
||||
const referencedHeader = container.querySelector(
|
||||
`#${CSS.escape(labelledBy)}`,
|
||||
);
|
||||
expect(referencedHeader).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('render cell bars properly, and only when it is toggled on in both regular and percent metrics', () => {
|
||||
const props = transformProps({
|
||||
...testData.raw,
|
||||
rawFormData: { ...testData.raw.rawFormData },
|
||||
@@ -572,7 +782,7 @@ describe('plugin-chart-table', () => {
|
||||
cells = document.querySelectorAll('td');
|
||||
});
|
||||
|
||||
it('render color with string column color formatter(operator begins with)', () => {
|
||||
test('render color with string column color formatter(operator begins with)', () => {
|
||||
render(
|
||||
ProviderWrapper({
|
||||
children: (
|
||||
@@ -604,7 +814,7 @@ describe('plugin-chart-table', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('render color with string column color formatter (operator ends with)', () => {
|
||||
test('render color with string column color formatter (operator ends with)', () => {
|
||||
render(
|
||||
ProviderWrapper({
|
||||
children: (
|
||||
@@ -633,7 +843,7 @@ describe('plugin-chart-table', () => {
|
||||
expect(getComputedStyle(screen.getByText('Joe')).background).toBe('');
|
||||
});
|
||||
|
||||
it('render color with string column color formatter (operator containing)', () => {
|
||||
test('render color with string column color formatter (operator containing)', () => {
|
||||
render(
|
||||
ProviderWrapper({
|
||||
children: (
|
||||
@@ -662,7 +872,7 @@ describe('plugin-chart-table', () => {
|
||||
expect(getComputedStyle(screen.getByText('Joe')).background).toBe('');
|
||||
});
|
||||
|
||||
it('render color with string column color formatter (operator not containing)', () => {
|
||||
test('render color with string column color formatter (operator not containing)', () => {
|
||||
render(
|
||||
ProviderWrapper({
|
||||
children: (
|
||||
@@ -693,7 +903,7 @@ describe('plugin-chart-table', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('render color with string column color formatter (operator =)', () => {
|
||||
test('render color with string column color formatter (operator =)', () => {
|
||||
render(
|
||||
ProviderWrapper({
|
||||
children: (
|
||||
@@ -724,7 +934,7 @@ describe('plugin-chart-table', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('render color with string column color formatter (operator None)', () => {
|
||||
test('render color with string column color formatter (operator None)', () => {
|
||||
render(
|
||||
ProviderWrapper({
|
||||
children: (
|
||||
@@ -757,7 +967,7 @@ describe('plugin-chart-table', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('render color with column color formatter to entire row', () => {
|
||||
test('render color with column color formatter to entire row', () => {
|
||||
render(
|
||||
ProviderWrapper({
|
||||
children: (
|
||||
@@ -793,7 +1003,7 @@ describe('plugin-chart-table', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('display text color using column color formatter', () => {
|
||||
test('display text color using column color formatter', () => {
|
||||
render(
|
||||
ProviderWrapper({
|
||||
children: (
|
||||
@@ -826,7 +1036,7 @@ describe('plugin-chart-table', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('display text color using column color formatter for entire row', () => {
|
||||
test('display text color using column color formatter for entire row', () => {
|
||||
render(
|
||||
ProviderWrapper({
|
||||
children: (
|
||||
|
||||
@@ -171,7 +171,7 @@ class ChartDataRestApi(ChartRestApi):
|
||||
and query_context.result_format == ChartDataResultFormat.JSON
|
||||
and query_context.result_type == ChartDataResultType.FULL
|
||||
):
|
||||
return self._run_async(json_body, command)
|
||||
return self._run_async(json_body, command, add_extra_log_payload)
|
||||
|
||||
try:
|
||||
form_data = json.loads(chart.params)
|
||||
@@ -265,7 +265,7 @@ class ChartDataRestApi(ChartRestApi):
|
||||
and query_context.result_format == ChartDataResultFormat.JSON
|
||||
and query_context.result_type == ChartDataResultType.FULL
|
||||
):
|
||||
return self._run_async(json_body, command)
|
||||
return self._run_async(json_body, command, add_extra_log_payload)
|
||||
|
||||
form_data = json_body.get("form_data")
|
||||
return self._get_data_response(
|
||||
@@ -334,7 +334,10 @@ class ChartDataRestApi(ChartRestApi):
|
||||
return self._get_data_response(command, True)
|
||||
|
||||
def _run_async(
|
||||
self, form_data: dict[str, Any], command: ChartDataCommand
|
||||
self,
|
||||
form_data: dict[str, Any],
|
||||
command: ChartDataCommand,
|
||||
add_extra_log_payload: Callable[..., None] | None = None,
|
||||
) -> Response:
|
||||
"""
|
||||
Execute command as an async query.
|
||||
@@ -343,6 +346,10 @@ class ChartDataRestApi(ChartRestApi):
|
||||
with contextlib.suppress(ChartDataCacheLoadError):
|
||||
result = command.run(force_cached=True)
|
||||
if result is not None:
|
||||
# Log is_cached if extra payload callback is provided.
|
||||
# This indicates no async job was triggered - data was already cached
|
||||
# and a synchronous response is being returned immediately.
|
||||
self._log_is_cached(result, add_extra_log_payload)
|
||||
return self._send_chart_response(result)
|
||||
# Otherwise, kick off a background job to run the chart query.
|
||||
# Clients will either poll or be notified of query completion,
|
||||
@@ -424,6 +431,25 @@ class ChartDataRestApi(ChartRestApi):
|
||||
|
||||
return self.response_400(message=f"Unsupported result_format: {result_format}")
|
||||
|
||||
def _log_is_cached(
|
||||
self,
|
||||
result: dict[str, Any],
|
||||
add_extra_log_payload: Callable[..., None] | None,
|
||||
) -> None:
|
||||
"""
|
||||
Log is_cached values from query results to event logger.
|
||||
|
||||
Extracts is_cached from each query in the result and logs it.
|
||||
If there's a single query, logs the boolean value directly.
|
||||
If multiple queries, logs as a list.
|
||||
"""
|
||||
if add_extra_log_payload and result and "queries" in result:
|
||||
is_cached_values = [query.get("is_cached") for query in result["queries"]]
|
||||
if len(is_cached_values) == 1:
|
||||
add_extra_log_payload(is_cached=is_cached_values[0])
|
||||
elif is_cached_values:
|
||||
add_extra_log_payload(is_cached=is_cached_values)
|
||||
|
||||
@event_logger.log_this
|
||||
def _get_data_response(
|
||||
self,
|
||||
@@ -442,12 +468,7 @@ class ChartDataRestApi(ChartRestApi):
|
||||
return self.response_400(message=exc.message)
|
||||
|
||||
# Log is_cached if extra payload callback is provided
|
||||
if add_extra_log_payload and result and "queries" in result:
|
||||
is_cached_values = [query.get("is_cached") for query in result["queries"]]
|
||||
if len(is_cached_values) == 1:
|
||||
add_extra_log_payload(is_cached=is_cached_values[0])
|
||||
elif is_cached_values:
|
||||
add_extra_log_payload(is_cached=is_cached_values)
|
||||
self._log_is_cached(result, add_extra_log_payload)
|
||||
|
||||
return self._send_chart_response(result, form_data, datasource)
|
||||
|
||||
|
||||
@@ -1502,8 +1502,14 @@ class SqlaTable(
|
||||
"""
|
||||
label = utils.get_column_name(col)
|
||||
try:
|
||||
sql_expression = col["sqlExpression"]
|
||||
|
||||
# For column references, conditionally quote identifiers that need it
|
||||
if col.get("isColumnReference"):
|
||||
sql_expression = self.database.quote_identifier(sql_expression)
|
||||
|
||||
expression = self._process_select_expression(
|
||||
expression=col["sqlExpression"],
|
||||
expression=sql_expression,
|
||||
database_id=self.database_id,
|
||||
engine=self.database.backend,
|
||||
schema=self.schema,
|
||||
|
||||
@@ -337,16 +337,32 @@ class SqlLabRestApi(BaseSupersetApi):
|
||||
params = kwargs["rison"]
|
||||
key = params.get("key")
|
||||
rows = params.get("rows")
|
||||
result = SqlExecutionResultsCommand(key=key, rows=rows).run()
|
||||
|
||||
try:
|
||||
result = SqlExecutionResultsCommand(key=key, rows=rows).run()
|
||||
except Exception as ex:
|
||||
logger.exception("Error fetching query results for key=%s", key)
|
||||
return self.response_500(message=str(ex))
|
||||
|
||||
# Using pessimistic json serialization since some database drivers can return
|
||||
# unserializeable types at times
|
||||
payload = json.dumps(
|
||||
result,
|
||||
default=json.pessimistic_json_iso_dttm_ser,
|
||||
ignore_nan=True,
|
||||
)
|
||||
return json_success(payload, 200)
|
||||
try:
|
||||
payload = json.dumps(
|
||||
result,
|
||||
default=json.pessimistic_json_iso_dttm_ser,
|
||||
ignore_nan=True,
|
||||
)
|
||||
except Exception as ex:
|
||||
logger.exception("Error serializing query results for key=%s", key)
|
||||
return self.response_500(message="Unable to serialize query results")
|
||||
|
||||
# Use json_success with explicit Content-Type to ensure Flask 2.3+ correctly
|
||||
# handles the response and doesn't trigger HTTP 406 errors due to content
|
||||
# negotiation issues with Accept headers or proxy configurations
|
||||
response = json_success(payload, 200)
|
||||
# Explicitly set Content-Type as a safeguard against content negotiation issues
|
||||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
return response
|
||||
|
||||
@expose("/execute/", methods=("POST",))
|
||||
@protect()
|
||||
@@ -410,8 +426,11 @@ class SqlLabRestApi(BaseSupersetApi):
|
||||
if command_result["status"] == SqlJsonExecutionStatus.QUERY_IS_RUNNING
|
||||
else 200
|
||||
)
|
||||
# return the execution result without special encoding
|
||||
return json_success(command_result["payload"], response_status)
|
||||
# Return the execution result without special encoding
|
||||
# Set explicit Content-Type to prevent Flask 2.3+ content negotiation issues
|
||||
response = json_success(command_result["payload"], response_status)
|
||||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
return response
|
||||
except SqlLabException as ex:
|
||||
payload = {"errors": [ex.to_dict()]}
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ class AdhocColumn(TypedDict, total=False):
|
||||
hasCustomLabel: Optional[bool]
|
||||
label: str
|
||||
sqlExpression: str
|
||||
isColumnReference: Optional[bool]
|
||||
columnType: Optional[Literal["BASE_AXIS", "SERIES"]]
|
||||
timeGrain: Optional[str]
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ msgstr ""
|
||||
"POT-Creation-Date: 2025-04-29 12:34+0330\n"
|
||||
"PO-Revision-Date: 2016-05-02 08:49-0700\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: en\n"
|
||||
"Language-Team: en <LL@li.org>\n"
|
||||
"Language: es\n"
|
||||
"Language-Team: Español; Castellano <>\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -91,6 +91,10 @@ msgstr " uno nuevo"
|
||||
msgid " at line %(line)d"
|
||||
msgstr " en la línea %(line)d"
|
||||
|
||||
#, python-format
|
||||
msgid " at line %(line)d"
|
||||
msgstr ""
|
||||
|
||||
msgid " expression which needs to adhere to the "
|
||||
msgstr " expresión que debe adherirse al "
|
||||
|
||||
@@ -184,7 +188,14 @@ msgstr "la frecuencia de %(report_type)s programación excede el límite. Config
|
||||
|
||||
#, python-format
|
||||
msgid "%(rows)d rows returned"
|
||||
msgstr "%(rows)d filas devueltas"
|
||||
msgstr "líneas obtenidas"
|
||||
|
||||
#, python-format
|
||||
msgid ""
|
||||
"%(subtitle)s\n"
|
||||
"This may be triggered by:\n"
|
||||
" %(issue)s"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy, python-format
|
||||
msgid "%(suggestion)s instead of \"%(undefinedParameter)s?\""
|
||||
@@ -255,7 +266,7 @@ msgstr "%s elementos no se han podido etiquetar porque no tienes permisos de edi
|
||||
msgid "%s operator(s)"
|
||||
msgstr "%s operador(es)"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "%s option"
|
||||
msgid_plural "%s options"
|
||||
msgstr[0] "%s opción"
|
||||
@@ -269,7 +280,7 @@ msgstr "%s opción(es)"
|
||||
msgid "%s recipients"
|
||||
msgstr "%s destinatarios"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "%s row"
|
||||
msgid_plural "%s rows"
|
||||
msgstr[0] "%s fila"
|
||||
@@ -1065,14 +1076,14 @@ msgstr "La consulta de alerta ha devuelto más de una columna."
|
||||
|
||||
#, python-format
|
||||
msgid "Alert query returned more than one column. %(num_cols)s columns returned"
|
||||
msgstr "La consulta de alerta ha devuelto más de una columna. %(num_cols)s columnas devueltas"
|
||||
msgstr "La consulta de alerta devolvió más de una columna. %(num_cols)s columnas devueltas"
|
||||
|
||||
msgid "Alert query returned more than one row."
|
||||
msgstr "La consulta de alerta ha devuelto más de una fila."
|
||||
|
||||
#, python-format
|
||||
msgid "Alert query returned more than one row. %(num_rows)s rows returned"
|
||||
msgstr "La consulta de alerta ha devuelto más de una fila. %(num_rows)s filas devueltas"
|
||||
msgstr "La consulta de alerta devolvió más de una fila. %(num_rows)s filas devueltas"
|
||||
|
||||
msgid "Alert running"
|
||||
msgstr "Alerta en ejecución"
|
||||
@@ -1242,7 +1253,7 @@ msgid "An error occurred while creating %ss: %s"
|
||||
msgstr "Se ha producido un error al crear %ss: %s"
|
||||
|
||||
msgid "An error occurred while creating the copy link."
|
||||
msgstr "Se ha producido un error al crear el enlace de copia."
|
||||
msgstr "Se produjo un error en la creación %ss: %s"
|
||||
|
||||
msgid "An error occurred while creating the data source"
|
||||
msgstr "Se ha producido un error al crear la fuente de datos"
|
||||
@@ -1339,7 +1350,7 @@ msgstr "Se ha producido un error al recuperar los valores del usuario: %s"
|
||||
|
||||
#, python-format
|
||||
msgid "An error occurred while importing %s: %s"
|
||||
msgstr "Se ha producido un error al importar %s: %s"
|
||||
msgstr "Se produjo un error importando %s: %s"
|
||||
|
||||
msgid "An error occurred while loading dashboard information."
|
||||
msgstr "Se ha producido un error al cargar la información del panel de control."
|
||||
@@ -1535,7 +1546,7 @@ msgstr "Filtros aplicados (%s)"
|
||||
|
||||
#, python-format
|
||||
msgid "Applied filters: %s"
|
||||
msgstr "Filtros aplicados: %s"
|
||||
msgstr "Filtros aplicados %s"
|
||||
|
||||
msgid ""
|
||||
"Applied rolling window did not return any data. Please make sure the "
|
||||
@@ -1577,7 +1588,7 @@ msgstr "¿Seguro que quieres eliminar?"
|
||||
|
||||
#, python-format
|
||||
msgid "Are you sure you want to delete %s?"
|
||||
msgstr "¿Seguro que quieres eliminar %s?"
|
||||
msgstr "¿Está seguro de que desea eliminar %s?"
|
||||
|
||||
#, python-format
|
||||
msgid "Are you sure you want to delete the selected %s?"
|
||||
@@ -2238,7 +2249,7 @@ msgstr "Opciones del gráfico"
|
||||
msgid "Chart Orientation"
|
||||
msgstr "Orientación del gráfico"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "Chart Owner: %s"
|
||||
msgid_plural "Chart Owners: %s"
|
||||
msgstr[0] "Propietario del gráfico: %s"
|
||||
@@ -2252,15 +2263,15 @@ msgstr "Título del gráfico"
|
||||
|
||||
#, python-format
|
||||
msgid "Chart [%s] has been overwritten"
|
||||
msgstr "El gráfico [%s] se ha sobrescrito"
|
||||
msgstr "El gráfico [%s] ha sido sobreescrito"
|
||||
|
||||
#, python-format
|
||||
msgid "Chart [%s] has been saved"
|
||||
msgstr "El gráfico [%s] se ha guardado"
|
||||
msgstr "El gráfico [%s] ha sido guardado"
|
||||
|
||||
#, python-format
|
||||
msgid "Chart [%s] was added to dashboard [%s]"
|
||||
msgstr "El gráfico [%s] se ha añadido al panel de control [%s]"
|
||||
msgstr "El gráfico [%s] ha sido añadido al panel de control [%s]"
|
||||
|
||||
msgid "Chart [{}] has been overwritten"
|
||||
msgstr "El gráfico [{}] se ha sobrescrito"
|
||||
@@ -2796,7 +2807,7 @@ msgid "Configuration"
|
||||
msgstr "Configuración"
|
||||
|
||||
msgid "Configure Advanced Time Range "
|
||||
msgstr "Configurar el intervalo de tiempo avanzado "
|
||||
msgstr "Configuración avanzada de rango de tiempo "
|
||||
|
||||
msgid "Configure Time Range: Current..."
|
||||
msgstr "Configurar el intervalo de tiempo: actual..."
|
||||
@@ -2805,7 +2816,7 @@ msgid "Configure Time Range: Last..."
|
||||
msgstr "Configurar el intervalo de tiempo: último..."
|
||||
|
||||
msgid "Configure Time Range: Previous..."
|
||||
msgstr "Configurar el intervalo de tiempo: anterior..."
|
||||
msgstr "Configurar Rango de Tiempo: Anteriores..."
|
||||
|
||||
msgid "Configure custom time range"
|
||||
msgstr "Configurar intervalo de tiempo personalizado"
|
||||
@@ -3814,7 +3825,7 @@ msgid_plural "Deleted %(num)d report schedules"
|
||||
msgstr[0] "Se ha eliminado%(num)d programación de informe"
|
||||
msgstr[1] "Se han eliminado%(num)d programaciones de informe"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "Deleted %(num)d rules"
|
||||
msgid_plural "Deleted %(num)d rules"
|
||||
msgstr[0] "Se han eliminado%(num)d reglas"
|
||||
@@ -4112,13 +4123,11 @@ msgstr "El desglose en detalle está deshabilitado para esta base de datos. Camb
|
||||
msgid "Drill to detail: %s"
|
||||
msgstr "Desglosar en detalle: %s"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Drop a column here or click"
|
||||
msgid_plural "Drop columns here or click"
|
||||
msgstr[0] "Suelta una columna aquí o haz clic"
|
||||
msgstr[1] "Suelta las columnas aquí o haz clic"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Drop a column/metric here or click"
|
||||
msgid_plural "Drop columns/metrics here or click"
|
||||
msgstr[0] "Suelta una columna/métrica aquí o haz clic"
|
||||
@@ -4185,10 +4194,10 @@ msgid "Duration in ms (1.40008 => 1ms 400µs 80ns)"
|
||||
msgstr "Duración en ms (1,40008 => 1 ms 400 µs 80 ns)"
|
||||
|
||||
msgid "Duration in ms (100.40008 => 100ms 400µs 80ns)"
|
||||
msgstr "Duración en ms (100,40008 => 100 ms 400 µs 80 ns)"
|
||||
msgstr "Duración en ms (100.40008 => 100 ms 400 µs 80 ns)"
|
||||
|
||||
msgid "Duration in ms (10500 => 0:10.5)"
|
||||
msgstr "Duración en ms (10 500 => 0:10,5)"
|
||||
msgstr "Duración en ms (10500 => 0:10.5)"
|
||||
|
||||
msgid "Duration in ms (66000 => 1m 6s)"
|
||||
msgstr "Duración en ms (66 000 => 1 m 6 s)"
|
||||
@@ -4235,6 +4244,9 @@ msgstr "Editar CSS"
|
||||
msgid "Edit CSS template properties"
|
||||
msgstr "Editar propiedades de la plantilla CSS"
|
||||
|
||||
msgid "Edit Chart"
|
||||
msgstr "Editar Gráfico"
|
||||
|
||||
msgid "Edit Chart Properties"
|
||||
msgstr "Editar propiedades del gráfico"
|
||||
|
||||
@@ -4369,7 +4381,7 @@ msgid "Embed dashboard"
|
||||
msgstr "Incrustar panel de control"
|
||||
|
||||
msgid "Embedded dashboard could not be deleted."
|
||||
msgstr "No se ha podido eliminar el panel de control incrustado."
|
||||
msgstr "El panel de control no pudo ser eliminado."
|
||||
|
||||
msgid "Embedding deactivated."
|
||||
msgstr "Incrustación desactivada."
|
||||
@@ -4453,7 +4465,7 @@ msgid "End date"
|
||||
msgstr "Fecha final"
|
||||
|
||||
msgid "End date excluded from time range"
|
||||
msgstr "Fecha final excluida del intervalo de tiempo"
|
||||
msgstr "Fecha final excluida del rango de tiempo"
|
||||
|
||||
msgid "End date must be after start date"
|
||||
msgstr "La fecha final debe ser posterior a la fecha inicial"
|
||||
@@ -4572,9 +4584,6 @@ msgstr "Error al leer el archivo de Excel"
|
||||
msgid "Error saving dataset"
|
||||
msgstr "Error al guardar el conjunto de datos"
|
||||
|
||||
msgid "Error unfaving chart"
|
||||
msgstr "Error al quitar el gráfico de favoritos"
|
||||
|
||||
msgid "Error while adding role!"
|
||||
msgstr "Error al añadir el rol"
|
||||
|
||||
@@ -4584,6 +4593,9 @@ msgstr "Error al añadir el usuario"
|
||||
msgid "Error while duplicating role!"
|
||||
msgstr "Error al duplicar el rol"
|
||||
|
||||
msgid "Error unfaving chart"
|
||||
msgstr "Error al quitar el gráfico de favoritos"
|
||||
|
||||
msgid "Error while fetching charts"
|
||||
msgstr "Error al recuperar los gráficos"
|
||||
|
||||
@@ -5218,10 +5230,10 @@ msgid "Geometry Column"
|
||||
msgstr "Columna de geometría"
|
||||
|
||||
msgid "Get the last date by the date unit."
|
||||
msgstr "Obtener la última fecha por la unidad de fecha."
|
||||
msgstr "Obtiene la última fecha para la unidad de fecha especificada."
|
||||
|
||||
msgid "Get the specify date for the holiday"
|
||||
msgstr "Obtener la fecha especificada para el día festivo"
|
||||
msgstr "Obtiene la fecha del día feriado especificado"
|
||||
|
||||
msgid "Give access to multiple catalogs in a single database connection."
|
||||
msgstr "Da acceso a múltiples catálogos en una sola conexión de base de datos."
|
||||
@@ -5496,7 +5508,7 @@ msgstr "Incluye una descripción que se enviará con tu informe"
|
||||
|
||||
#, python-format
|
||||
msgid "Include description to be sent with %s"
|
||||
msgstr "Incluye una descripción para enviarse con %s"
|
||||
msgstr "Incluye una descripción para ser enviada con %s"
|
||||
|
||||
msgid "Include series name as an axis"
|
||||
msgstr "Incluir el nombre de la serie como eje"
|
||||
@@ -5584,7 +5596,7 @@ msgstr "JSON no válido"
|
||||
|
||||
#, python-format
|
||||
msgid "Invalid advanced data type: %(advanced_data_type)s"
|
||||
msgstr "Tipo de datos avanzados no válido: %(advanced_data_type)s"
|
||||
msgstr "Tipo de información avanzada inválida: %(advanced_data_type)s"
|
||||
|
||||
msgid "Invalid certificate"
|
||||
msgstr "Certificado no válido"
|
||||
@@ -5657,7 +5669,7 @@ msgstr "Referencia no válida a la columna: «%(column)s»"
|
||||
|
||||
#, python-format
|
||||
msgid "Invalid result type: %(result_type)s"
|
||||
msgstr "Tipo de resultado no válido: %(result_type)s"
|
||||
msgstr "Tipo de resultado inválido: %(result_type)s"
|
||||
|
||||
#, python-format
|
||||
msgid "Invalid rolling_type: %(type)s"
|
||||
@@ -5665,7 +5677,7 @@ msgstr "Tipo móvil no válido: %(type)s "
|
||||
|
||||
#, python-format
|
||||
msgid "Invalid spatial point encountered: %(latlong)s"
|
||||
msgstr "Se ha encontrado un punto espacial no válido: %(latlong)s"
|
||||
msgstr "Se encontró un punto espacial inválido: %(latlong)s"
|
||||
|
||||
msgid "Invalid state."
|
||||
msgstr "Estado no válido."
|
||||
@@ -6451,7 +6463,7 @@ msgid "Middle"
|
||||
msgstr "Medio"
|
||||
|
||||
msgid "Midnight"
|
||||
msgstr "Medianoche"
|
||||
msgstr "Media noche"
|
||||
|
||||
msgid "Miles"
|
||||
msgstr "Millas"
|
||||
@@ -6562,7 +6574,7 @@ msgstr "Mes"
|
||||
|
||||
#, python-format
|
||||
msgid "Months %s"
|
||||
msgstr "Meses %s "
|
||||
msgstr "Meses %s"
|
||||
|
||||
msgid "More"
|
||||
msgstr "Más"
|
||||
@@ -6577,7 +6589,7 @@ msgid "Move only"
|
||||
msgstr "Solo mover"
|
||||
|
||||
msgid "Moves the given set of dates by a specified interval."
|
||||
msgstr "Mueve el conjunto de fechas en cuestión por un intervalo especificado."
|
||||
msgstr "Desplaza el conjunto de fechas dado en un intervalo especificado."
|
||||
|
||||
msgid "Multi-Dimensions"
|
||||
msgstr "Multidimensional"
|
||||
@@ -6797,7 +6809,7 @@ msgid "No entities have this tag currently assigned"
|
||||
msgstr "Ninguna entidad tiene esta etiqueta asignada actualmente"
|
||||
|
||||
msgid "No filter"
|
||||
msgstr "No hay ningún filtro"
|
||||
msgstr "Sin filtro"
|
||||
|
||||
msgid "No filter is selected."
|
||||
msgstr "No se ha seleccionado ningún filtro."
|
||||
@@ -6821,7 +6833,7 @@ msgid "No records found"
|
||||
msgstr "No se han encontrado registros"
|
||||
|
||||
msgid "No results"
|
||||
msgstr "No hay resultados"
|
||||
msgstr "Sin resultados"
|
||||
|
||||
msgid "No results found"
|
||||
msgstr "No se han encontrado resultados"
|
||||
@@ -6830,7 +6842,7 @@ msgid "No results match your filter criteria"
|
||||
msgstr "No hay resultados que coincidan con tus criterios de filtro"
|
||||
|
||||
msgid "No results were returned for this query"
|
||||
msgstr "No se han devuelto resultados para esta consulta"
|
||||
msgstr "No se obtuvieron resultados para esta consulta"
|
||||
|
||||
msgid ""
|
||||
"No results were returned for this query. If you expected results to be "
|
||||
@@ -7073,7 +7085,7 @@ msgid "One or many metrics to display"
|
||||
msgstr "Una o varias métricas a mostrar"
|
||||
|
||||
msgid "One or more annotation layers failed loading."
|
||||
msgstr "No se han podido cargar una o más capas de anotación."
|
||||
msgstr "Una o más capas de anotación fallaron al cargar."
|
||||
|
||||
msgid "One or more columns already exist"
|
||||
msgstr "Una o más columnas ya existen"
|
||||
@@ -7489,7 +7501,7 @@ msgid "Pie Chart"
|
||||
msgstr "Gráfico tipo pastel"
|
||||
|
||||
msgid "Pie charts on a map"
|
||||
msgstr "Gráficos tipo pastel en un mapa"
|
||||
msgstr "Mapa con gráficos tipo pastel"
|
||||
|
||||
msgid "Pie shape"
|
||||
msgstr "Forma de pastel"
|
||||
@@ -7590,7 +7602,6 @@ msgstr "Vuelve a introducir tu contraseña."
|
||||
msgid "Please re-export your file and try importing again"
|
||||
msgstr "Vuelve a exportar tu archivo e intenta importarlo de nuevo"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Please reach out to the Chart Owner for assistance."
|
||||
msgid_plural "Please reach out to the Chart Owners for assistance."
|
||||
msgstr[0] "Ponte en contacto con el propietario del gráfico para obtener ayuda."
|
||||
@@ -8007,7 +8018,7 @@ msgid "Relationships between community channels"
|
||||
msgstr "Relaciones entre canales comunitarios"
|
||||
|
||||
msgid "Relative Date/Time"
|
||||
msgstr "Fecha/hora relativa"
|
||||
msgstr "Fecha/Hora Relativa"
|
||||
|
||||
msgid "Relative period"
|
||||
msgstr "Periodo relativo"
|
||||
@@ -8752,6 +8763,9 @@ msgstr "Selecciona el método de entrega"
|
||||
msgid "Select Tags"
|
||||
msgstr "Seleccionar etiquetas"
|
||||
|
||||
msgid "Select Viz Type"
|
||||
msgstr "Selecciona un tipo de visualización"
|
||||
|
||||
msgid "Select chart type"
|
||||
msgstr "Seleccionar tipo de visualización"
|
||||
|
||||
@@ -9141,12 +9155,21 @@ msgstr "Mostrar burbujas"
|
||||
msgid "Show CREATE VIEW statement"
|
||||
msgstr "Mostrar instrucción CREAR VISTA"
|
||||
|
||||
msgid "Show cell bars"
|
||||
msgstr "Mostrar barras de celda"
|
||||
msgid "Show Cell bars"
|
||||
msgstr "Todos los gráficos"
|
||||
|
||||
msgid "Show Chart"
|
||||
msgstr "Mostrar Gráfico"
|
||||
|
||||
msgid "Show Column"
|
||||
msgstr "Mostrar Columna"
|
||||
|
||||
msgid "Show Dashboard"
|
||||
msgstr "Mostrar el panel de control"
|
||||
|
||||
msgid "Show Database"
|
||||
msgstr "Mostrar Base de Datos"
|
||||
|
||||
msgid "Show Labels"
|
||||
msgstr "Mostrar etiquetas"
|
||||
|
||||
@@ -9156,6 +9179,9 @@ msgstr "Mostrar registro"
|
||||
msgid "Show Markers"
|
||||
msgstr "Mostrar marcadores"
|
||||
|
||||
msgid "Show Metric"
|
||||
msgstr "Mostrar Métrica"
|
||||
|
||||
msgid "Show Metric Names"
|
||||
msgstr "Mostrar nombres de las métricas"
|
||||
|
||||
@@ -9423,7 +9449,7 @@ msgstr "Lo sentimos, se ha producido un error. Inténtalo de nuevo más tarde."
|
||||
|
||||
#, python-format
|
||||
msgid "Sorry, there was an error saving this %s: %s"
|
||||
msgstr "Lo sentimos, se ha producido un error al guardar este %s: %s"
|
||||
msgstr "Lo sentimos, se ha producido un error al guardar esto %s: %s"
|
||||
|
||||
#, python-format
|
||||
msgid "Sorry, there was an error saving this dashboard: %s"
|
||||
@@ -9512,7 +9538,7 @@ msgid "Spatial"
|
||||
msgstr "Espacial"
|
||||
|
||||
msgid "Specific Date/Time"
|
||||
msgstr "Fecha/hora específica"
|
||||
msgstr "Fecha/Hora Específica"
|
||||
|
||||
msgid "Specify name to CREATE TABLE AS schema in: public"
|
||||
msgstr "Especifica el nombre para el esquema CREAR TABLA COMO en: público"
|
||||
@@ -9577,7 +9603,7 @@ msgid "Start date"
|
||||
msgstr "Fecha de inicio"
|
||||
|
||||
msgid "Start date included in time range"
|
||||
msgstr "Fecha de inicio incluida en el intervalo de tiempo"
|
||||
msgstr "Fecha inicial incluida en el rango de tiempo"
|
||||
|
||||
msgid "Start y-axis at 0"
|
||||
msgstr "Iniciar eje Y en 0"
|
||||
@@ -9733,6 +9759,9 @@ msgstr "Documentación del SDK integrado de Superset."
|
||||
msgid "Superset chart"
|
||||
msgstr "Gráfico Superset"
|
||||
|
||||
msgid "Superset dashboard"
|
||||
msgstr "Dashboard Superset"
|
||||
|
||||
msgid "Superset encountered an error while running a command."
|
||||
msgstr "Superset ha encontrado un error al ejecutar un comando."
|
||||
|
||||
@@ -9850,7 +9879,7 @@ msgstr "No se ha definido el nombre de la tabla"
|
||||
|
||||
#, python-format
|
||||
msgid "Table or View \"%(table)s\" does not exist."
|
||||
msgstr "La tabla o la vista «%(table)s» no existen."
|
||||
msgstr "La tabla o vista \"%(table)s\" no existe"
|
||||
|
||||
msgid ""
|
||||
"Table that visualizes paired t-tests, which are used to understand "
|
||||
@@ -10717,7 +10746,9 @@ msgid "There was an error loading the tables"
|
||||
msgstr "Se ha producido un error al cargar las tablas"
|
||||
|
||||
msgid "There was an error retrieving dashboard tabs."
|
||||
msgstr "Se ha producido un error al recuperar las pestañas del panel."
|
||||
msgstr ""
|
||||
"Lo sentimos, hubo un error al obtener la información de la base de datos:"
|
||||
" %s"
|
||||
|
||||
#, python-format
|
||||
msgid "There was an error saving the favorite status: %s"
|
||||
@@ -10736,7 +10767,7 @@ msgstr "Ha habido un problema al eliminar %s: %s"
|
||||
|
||||
#, python-format
|
||||
msgid "There was an issue deleting rules: %s"
|
||||
msgstr "Ha habido un problema al eliminar las reglas: %s"
|
||||
msgstr "Hubo un problema eliminando las reglas: %s"
|
||||
|
||||
#, python-format
|
||||
msgid "There was an issue deleting the selected %s: %s"
|
||||
@@ -10795,11 +10826,11 @@ msgstr "Ha habido un problema al recuperar tu gráfico: %s "
|
||||
|
||||
#, python-format
|
||||
msgid "There was an issue fetching your dashboards: %s"
|
||||
msgstr "Ha habido un problema al recuperar tus paneles de control: %s"
|
||||
msgstr "Hubo un problema al obtener tus dashboards: %s"
|
||||
|
||||
#, python-format
|
||||
msgid "There was an issue fetching your recent activity: %s"
|
||||
msgstr "Ha habido un problema al recuperar tu actividad reciente: %s"
|
||||
msgstr "Hubo un error al obtener tu actividad reciente: %s"
|
||||
|
||||
#, python-format
|
||||
msgid "There was an issue fetching your saved queries: %s"
|
||||
@@ -10839,7 +10870,7 @@ msgid "This action will permanently delete the template."
|
||||
msgstr "Esta acción eliminará permanentemente la plantilla."
|
||||
|
||||
msgid "This action will permanently delete the user."
|
||||
msgstr "Esta acción eliminará permanentemente el uduario."
|
||||
msgstr "Esta acción eliminará permanentemente el usuario."
|
||||
|
||||
msgid ""
|
||||
"This can be either an IP address (e.g. 127.0.0.1) or a domain name (e.g. "
|
||||
@@ -11048,7 +11079,6 @@ msgstr "Este tipo de visualización no admite el filtro cruzado."
|
||||
msgid "This visualization type is not supported."
|
||||
msgstr "Este tipo de visualización no se admite."
|
||||
|
||||
#, fuzzy
|
||||
msgid "This was triggered by:"
|
||||
msgid_plural "This may be triggered by:"
|
||||
msgstr[0] "La causa de esto ha sido:"
|
||||
@@ -11327,7 +11357,33 @@ msgid "Tree layout"
|
||||
msgstr "Diseño del árbol"
|
||||
|
||||
msgid "Tree orientation"
|
||||
msgstr "Orientación del árbol"
|
||||
Findings (brief):
|
||||
|
||||
- No git merge conflict markers found (no <<<<<<< / ======= / >>>>>>>).
|
||||
- PO header mismatch: "Language: en" — this is an Spanish file; set to "es".
|
||||
- Duplicate msgid entries with conflicting/empty translations:
|
||||
- " at line %(line)d" — one entry has " en la línea %(line)d", another has an empty msgstr.
|
||||
- "Dashboard cannot be copied due to invalid parameters." — appears multiple times with different/empty msgstr values.
|
||||
- "%(subtitle)s\nThis may be triggered by:\n %(issue)s" — msgstr is empty in one occurrence.
|
||||
- There are other repeated msgids with one occurrence left untranslated (examples: search for repeated msgid strings with one msgstr == "").
|
||||
- Empty translations (examples):
|
||||
- msgid "%(subtitle)s\nThis may be triggered by:\n %(issue)s" → msgstr "".
|
||||
- Several other msgid entries have msgstr "" (scan for msgstr "" occurrences).
|
||||
- Fuzzy entries present (e.g. entries annotated "#, fuzzy") — these need review and removal of the fuzzy flag after correction.
|
||||
- Typo in a translation: msgid "This action will permanently delete the user." → msgstr contains "uduario." (should be "usuario.").
|
||||
|
||||
Recommended next steps:
|
||||
- Fix header Language to "es".
|
||||
- Remove/fix duplicate msgids: consolidate into a single entry and keep the correct translation.
|
||||
- Fill in missing msgstr values (or mark as untranslated intentionally).
|
||||
- Review and resolve fuzzy entries, then remove the "fuzzy" flag.
|
||||
- Fix obvious typos (e.g., "uduario" → "usuario").
|
||||
|
||||
If you want, I can produce a patch that:
|
||||
- updates header Language to "es",
|
||||
- removes duplicate entries by keeping the first translated occurrence,
|
||||
- lists all msgids with empty msgstr for you to translate,
|
||||
or show exact locations (line ranges) for each problem. Which would you prefer?
|
||||
|
||||
msgid "Treemap"
|
||||
msgstr "Diagrama de árbol"
|
||||
@@ -11983,7 +12039,7 @@ msgstr "WMS"
|
||||
|
||||
#, python-format
|
||||
msgid "Waiting on %s"
|
||||
msgstr "Esperando a %s"
|
||||
msgstr "Esperando por %s"
|
||||
|
||||
msgid "Waiting on database..."
|
||||
msgstr "Esperando a la base de datos..."
|
||||
@@ -12083,7 +12139,7 @@ msgstr "Semanas %s"
|
||||
msgid "Weight"
|
||||
msgstr "Peso"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid ""
|
||||
"We’re having trouble loading these results. Queries are set to timeout "
|
||||
"after %s second."
|
||||
@@ -12093,7 +12149,7 @@ msgid_plural ""
|
||||
msgstr[0] "Estamos teniendo problemas para cargar estos resultados. Se considera que una consulta ha superado el tiempo de espera después de %s segundo."
|
||||
msgstr[1] "Estamos teniendo problemas para cargar estos resultados. Se considera que una consulta ha superado el tiempo de espera después de %s segundos."
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid ""
|
||||
"We’re having trouble loading this visualization. Queries are set to "
|
||||
"timeout after %s second."
|
||||
@@ -12576,7 +12632,7 @@ msgstr "No puedes utilizar el diseño de marca de 45° con el filtro de interval
|
||||
|
||||
#, python-format
|
||||
msgid "You do not have permission to edit this %s"
|
||||
msgstr "No tienes permisos para editar este %s"
|
||||
msgstr "No tienes permisos para editar esto %s"
|
||||
|
||||
msgid "You do not have permission to edit this chart"
|
||||
msgstr "No tienes permisos para editar este gráfico"
|
||||
@@ -12841,7 +12897,7 @@ msgid "background"
|
||||
msgstr "fondo"
|
||||
|
||||
msgid "Basic conditional formatting"
|
||||
msgstr "formato condicional básico"
|
||||
msgstr "Formato condicional básico"
|
||||
|
||||
msgid "basis"
|
||||
msgstr "base"
|
||||
@@ -13164,13 +13220,18 @@ msgid ""
|
||||
"is linked to %s charts that appear on %s dashboards and users have %s SQL"
|
||||
" Lab tabs using this database open. Are you sure you want to continue? "
|
||||
"Deleting the database will break those objects."
|
||||
msgstr "está vinculado a %s gráficos que aparecen en %s paneles de control y los usuarios tienen %s pestañas de SQL Lab usando esta base de datos abierta. ¿Seguro que quieres continuar? Eliminar la base de datos descompondrá esos objetos."
|
||||
msgstr ""
|
||||
"La base de datos %s está vinculada a %s gráficos que aparecen en %s "
|
||||
"dashboards. ¿Estás seguro de que quieres continuar? Eliminar la base de "
|
||||
"datos dejará inutilizables esos objetos."
|
||||
|
||||
#, python-format
|
||||
msgid ""
|
||||
"is linked to %s charts that appear on %s dashboards. Are you sure you "
|
||||
"want to continue? Deleting the dataset will break those objects."
|
||||
msgstr "está vinculado a %s gráficos que aparecen en %s paneles de control. ¿Seguro que quieres continuar? Eliminar el conjunto de datos descompondrá esos objetos."
|
||||
msgstr ""
|
||||
"esta linkeado a %s gráficos que aparecen en %s tableros. ¿Está seguro"
|
||||
"de que desea continuar? Eliminar el conjunto de datos romperá esos objetos."
|
||||
|
||||
msgid "is not"
|
||||
msgstr "no es"
|
||||
@@ -13310,16 +13371,19 @@ msgid "pixels"
|
||||
msgstr "píxeles"
|
||||
|
||||
msgid "previous calendar month"
|
||||
msgstr "mes natural anterior"
|
||||
msgstr "mes anterior"
|
||||
|
||||
msgid "previous calendar quarter"
|
||||
msgstr "trimestre natural anterior"
|
||||
msgstr "trimestre anterior"
|
||||
|
||||
msgid "previous calendar week"
|
||||
msgstr "semana natural anterior"
|
||||
msgstr "semana anterior"
|
||||
|
||||
msgid "previous calendar year"
|
||||
msgstr "año natural anterior"
|
||||
msgstr "año anterior"
|
||||
|
||||
msgid "published"
|
||||
msgstr "No publicado"
|
||||
|
||||
msgid "quarter"
|
||||
msgstr "trimestre"
|
||||
@@ -13339,6 +13403,9 @@ msgstr "reiniciar"
|
||||
msgid "recent"
|
||||
msgstr "reciente"
|
||||
|
||||
msgid "recents"
|
||||
msgstr "Recientes"
|
||||
|
||||
msgid "recipients"
|
||||
msgstr "destinatarios"
|
||||
|
||||
@@ -13360,6 +13427,9 @@ msgstr "rowlevelsecurity"
|
||||
msgid "running"
|
||||
msgstr "en ejecución"
|
||||
|
||||
msgid "saved queries"
|
||||
msgstr "Consultas Guardadas"
|
||||
|
||||
msgid "save"
|
||||
msgstr "guardar"
|
||||
|
||||
|
||||
@@ -753,10 +753,11 @@ class TestPostChartDataApi(BaseTestChartDataApi):
|
||||
|
||||
@with_feature_flags(GLOBAL_ASYNC_QUERIES=True)
|
||||
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
|
||||
def test_chart_data_async_cached_sync_response(self):
|
||||
@mock.patch("superset.extensions.event_logger.log")
|
||||
def test_chart_data_async_cached_sync_response(self, mock_event_logger):
|
||||
"""
|
||||
Chart data API: Test chart data query returns results synchronously
|
||||
when results are already cached.
|
||||
when results are already cached, and that is_cached is logged.
|
||||
"""
|
||||
app._got_first_request = False
|
||||
async_query_manager_factory.init_app(app)
|
||||
@@ -767,7 +768,7 @@ class TestPostChartDataApi(BaseTestChartDataApi):
|
||||
|
||||
cmd_run_val = {
|
||||
"query_context": QueryContext(),
|
||||
"queries": [{"query": "select * from foo"}],
|
||||
"queries": [{"query": "select * from foo", "is_cached": True}],
|
||||
}
|
||||
|
||||
with mock.patch.object(
|
||||
@@ -780,7 +781,16 @@ class TestPostChartDataApi(BaseTestChartDataApi):
|
||||
assert rv.status_code == 200
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
patched_run.assert_called_once_with(force_cached=True)
|
||||
assert data == {"result": [{"query": "select * from foo"}]}
|
||||
assert data == {
|
||||
"result": [{"query": "select * from foo", "is_cached": True}]
|
||||
}
|
||||
|
||||
# Verify that is_cached was logged to event logger
|
||||
call_kwargs = mock_event_logger.call_args[1]
|
||||
records = call_kwargs.get("records", [])
|
||||
assert len(records) > 0
|
||||
# is_cached should be True when retrieved from cache in async path
|
||||
assert records[0]["is_cached"] is True
|
||||
|
||||
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
|
||||
@mock.patch("superset.extensions.event_logger.log")
|
||||
|
||||
@@ -126,6 +126,41 @@ class TestSqlLab(SupersetTestCase):
|
||||
"engine_name": engine_name,
|
||||
}
|
||||
|
||||
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
|
||||
def test_sql_json_where_clause_content_type(self):
|
||||
"""
|
||||
Test that queries with WHERE clauses return proper Content-Type headers.
|
||||
|
||||
This test addresses issue #36072 where Flask 2.3+ content negotiation
|
||||
could cause HTTP 406 errors for queries with WHERE clauses, particularly
|
||||
when using ENABLE_PROXY_FIX or certain Accept header configurations.
|
||||
"""
|
||||
self.login(ADMIN_USERNAME)
|
||||
|
||||
# Test query with WHERE clause
|
||||
resp = self.client.post(
|
||||
"/api/v1/sqllab/execute/",
|
||||
json={
|
||||
"database_id": self.get_database_by_name("examples").id,
|
||||
"sql": "SELECT * FROM birth_names WHERE name = 'John' LIMIT 5",
|
||||
"client_id": "test_where_1",
|
||||
},
|
||||
)
|
||||
|
||||
# Verify response is successful
|
||||
assert resp.status_code in (200, 202), f"Expected 200/202, got {resp.status_code}"
|
||||
|
||||
# Verify Content-Type header is explicitly set to prevent 406 errors
|
||||
assert "application/json" in resp.headers.get("Content-Type", "")
|
||||
|
||||
# Verify response body is valid JSON
|
||||
data = resp.json
|
||||
assert isinstance(data, dict)
|
||||
|
||||
# If query ran synchronously (200), verify it has data
|
||||
if resp.status_code == 200:
|
||||
assert "data" in data or "query_id" in data
|
||||
|
||||
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
|
||||
def test_sql_json_dml_disallowed(self):
|
||||
self.login(ADMIN_USERNAME)
|
||||
|
||||
@@ -29,6 +29,8 @@ from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm.session import Session
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from superset.superset_typing import AdhocColumn
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from superset.models.core import Database
|
||||
|
||||
@@ -1125,3 +1127,34 @@ def test_process_select_expression_end_to_end(database: Database) -> None:
|
||||
assert expected.replace(" ", "").lower() in result.replace(" ", "").lower(), (
|
||||
f"Expected '{expected}' to be in result '{result}' for input '{expression}'"
|
||||
)
|
||||
|
||||
|
||||
def test_adhoc_column_to_sqla_with_column_reference(database: Database) -> None:
|
||||
"""
|
||||
Test that adhoc_column_to_sqla
|
||||
properly quotes column identifiers when isColumnReference is true.
|
||||
|
||||
This tests the fix for column names with spaces being properly quoted
|
||||
before being processed by SQLGlot to prevent "column AS alias" misinterpretation.
|
||||
"""
|
||||
from superset.connectors.sqla.models import SqlaTable
|
||||
|
||||
table = SqlaTable(
|
||||
table_name="test_table",
|
||||
database=database,
|
||||
)
|
||||
|
||||
# Test 1: Column reference with spaces should be quoted
|
||||
col_with_spaces: AdhocColumn = {
|
||||
"sqlExpression": "Customer Name",
|
||||
"label": "Customer Name",
|
||||
"isColumnReference": True,
|
||||
}
|
||||
|
||||
result = table.adhoc_column_to_sqla(col_with_spaces)
|
||||
|
||||
# Should contain the quoted column name
|
||||
assert result is not None
|
||||
result_str = str(result)
|
||||
|
||||
assert '"Customer Name"' in result_str
|
||||
|
||||
Reference in New Issue
Block a user