You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ar...@apache.org on 2024/02/22 13:43:50 UTC
(superset) branch master updated: refactor(plugins): Time Comparison Utils (#27145)
This is an automated email from the ASF dual-hosted git repository.
arivero pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 127df24c08 refactor(plugins): Time Comparison Utils (#27145)
127df24c08 is described below
commit 127df24c0837df0f6f7174ecb87a660dc1262458
Author: Antonio Rivero <38...@users.noreply.github.com>
AuthorDate: Thu Feb 22 14:43:43 2024 +0100
refactor(plugins): Time Comparison Utils (#27145)
---
.../packages/superset-ui-core/src/index.ts | 1 +
.../superset-ui-core/src/query/types/Query.ts | 2 +
.../superset-ui-core/src/time-comparison/README.md | 47 ++++
.../src/time-comparison/getComparisonFilters.ts | 67 +++++
.../src/time-comparison/getComparisonInfo.ts | 65 +++++
.../src/{validator => time-comparison}/index.ts | 11 +-
.../index.ts => time-comparison/types.ts} | 18 +-
.../superset-ui-core/src/validator/index.ts | 1 +
...dex.ts => validateTimeComparisonRangeValues.ts} | 25 +-
.../time-comparison/getComparisonFilters.test.ts | 144 +++++++++++
.../test/time-comparison/getComparisonInfo.test.ts | 174 +++++++++++++
.../time-comparison/index.test.ts} | 20 +-
.../validateTimeComparisonRangeValues.test.ts | 58 +++++
.../src/plugin/buildQuery.ts | 79 ++----
.../src/plugin/controlPanel.ts | 22 +-
.../src/plugin/transformProps.ts | 24 +-
.../src/utils.ts | 277 ---------------------
superset/charts/schemas.py | 8 +
superset/common/utils/time_range_utils.py | 3 +
superset/constants.py | 8 +
superset/utils/date_parser.py | 46 +++-
tests/unit_tests/utils/date_parser_tests.py | 72 ++++++
22 files changed, 779 insertions(+), 393 deletions(-)
diff --git a/superset-frontend/packages/superset-ui-core/src/index.ts b/superset-frontend/packages/superset-ui-core/src/index.ts
index ea7a4efde7..7258a3b648 100644
--- a/superset-frontend/packages/superset-ui-core/src/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/index.ts
@@ -37,3 +37,4 @@ export * from './math-expression';
export * from './ui-overrides';
export * from './hooks';
export * from './currency-format';
+export * from './time-comparison';
diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts
index 8999a2b574..718f10514c 100644
--- a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts
+++ b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts
@@ -69,6 +69,8 @@ export type QueryObjectExtras = Partial<{
time_grain_sqla?: TimeGranularity;
/** WHERE condition */
where?: string;
+ /** Instant Time Comparison */
+ instant_time_comparison_range?: string;
}>;
export type ResidualQueryObjectData = {
diff --git a/superset-frontend/packages/superset-ui-core/src/time-comparison/README.md b/superset-frontend/packages/superset-ui-core/src/time-comparison/README.md
new file mode 100644
index 0000000000..ccb0ac9e4b
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-core/src/time-comparison/README.md
@@ -0,0 +1,47 @@
+<!--
+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.
+-->
+
+## @superset-ui/core/time-comparison
+
+This is a collection of methods used to support Time Comparison in charts.
+
+#### Example usage
+
+```js
+import { getComparisonTimeRangeInfo } from '@superset-ui/core';
+const { since, until } = getComparisonTimeRangeInfo(
+ adhocFilters,
+ extraFormData,
+);
+console.log(adhocFilters, extraFormData);
+```
+
+or
+
+```js
+import { ComparisonTimeRangeType } from '@superset-ui/core';
+ComparisonTimeRangeType.Custom; // 'c'
+ComparisonTimeRangeType.InheritRange; // 'r'
+```
+
+#### API
+
+`fn(args)`
+
+- Do something
diff --git a/superset-frontend/packages/superset-ui-core/src/time-comparison/getComparisonFilters.ts b/superset-frontend/packages/superset-ui-core/src/time-comparison/getComparisonFilters.ts
new file mode 100644
index 0000000000..f58a9c7280
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-core/src/time-comparison/getComparisonFilters.ts
@@ -0,0 +1,67 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { QueryFormData } from '../query';
+import { AdhocFilter } from '../types';
+
+/**
+ * This method is used to get the query filters to be applied to the comparison query after
+ * overriding the time range in case an extra form data is provided.
+ * For example when rendering a chart that uses time comparison in a dashboard with time filters.
+ * @param formData - the form data
+ * @param extraFormData - the extra form data
+ * @returns the query filters to be applied to the comparison query
+ */
+export const getComparisonFilters = (
+ formData: QueryFormData,
+ extraFormData: any,
+): AdhocFilter[] => {
+ const timeFilterIndex: number =
+ formData.adhoc_filters?.findIndex(
+ filter => 'operator' in filter && filter.operator === 'TEMPORAL_RANGE',
+ ) ?? -1;
+
+ const timeFilter: AdhocFilter | null =
+ timeFilterIndex !== -1 && formData.adhoc_filters
+ ? formData.adhoc_filters[timeFilterIndex]
+ : null;
+
+ if (
+ timeFilter &&
+ 'comparator' in timeFilter &&
+ typeof timeFilter.comparator === 'string'
+ ) {
+ if (extraFormData?.time_range) {
+ timeFilter.comparator = extraFormData.time_range;
+ }
+ }
+
+ const comparisonQueryFilter = timeFilter ? [timeFilter] : [];
+
+ const otherFilters = formData.adhoc_filters?.filter(
+ (_value: any, index: number) => timeFilterIndex !== index,
+ );
+ const comparisonQueryFilters = otherFilters
+ ? [...comparisonQueryFilter, ...otherFilters]
+ : comparisonQueryFilter;
+
+ return comparisonQueryFilters;
+};
+
+export default getComparisonFilters;
diff --git a/superset-frontend/packages/superset-ui-core/src/time-comparison/getComparisonInfo.ts b/superset-frontend/packages/superset-ui-core/src/time-comparison/getComparisonInfo.ts
new file mode 100644
index 0000000000..f73167efde
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-core/src/time-comparison/getComparisonInfo.ts
@@ -0,0 +1,65 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { QueryFormData } from '../query';
+import { getComparisonFilters } from './getComparisonFilters';
+import { ComparisonTimeRangeType } from './types';
+
+/**
+ * This is the main function to get the comparison info. It will return the formData
+ * that a viz can use to query the comparison data and the time shift text needed for
+ * the comparison time range based on the control value.
+ * @param formData
+ * @param timeComparison
+ * @param extraFormData
+ * @returns the processed formData
+ */
+
+export const getComparisonInfo = (
+ formData: QueryFormData,
+ timeComparison: string,
+ extraFormData: any,
+): QueryFormData => {
+ let comparisonFormData;
+
+ if (timeComparison !== ComparisonTimeRangeType.Custom) {
+ comparisonFormData = {
+ ...formData,
+ adhoc_filters: getComparisonFilters(formData, extraFormData),
+ extra_form_data: {
+ ...extraFormData,
+ time_range: undefined,
+ },
+ };
+ } else {
+ // This is when user selects Custom as time comparison
+ comparisonFormData = {
+ ...formData,
+ adhoc_filters: formData.adhoc_custom,
+ extra_form_data: {
+ ...extraFormData,
+ time_range: undefined,
+ },
+ };
+ }
+
+ return comparisonFormData;
+};
+
+export default getComparisonInfo;
diff --git a/superset-frontend/packages/superset-ui-core/src/validator/index.ts b/superset-frontend/packages/superset-ui-core/src/time-comparison/index.ts
similarity index 62%
copy from superset-frontend/packages/superset-ui-core/src/validator/index.ts
copy to superset-frontend/packages/superset-ui-core/src/time-comparison/index.ts
index 6294bddec7..4b9fb361fd 100644
--- a/superset-frontend/packages/superset-ui-core/src/validator/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/time-comparison/index.ts
@@ -17,10 +17,7 @@
* under the License.
*/
-export { default as legacyValidateInteger } from './legacyValidateInteger';
-export { default as legacyValidateNumber } from './legacyValidateNumber';
-export { default as validateInteger } from './validateInteger';
-export { default as validateNumber } from './validateNumber';
-export { default as validateNonEmpty } from './validateNonEmpty';
-export { default as validateMaxValue } from './validateMaxValue';
-export { default as validateMapboxStylesUrl } from './validateMapboxStylesUrl';
+export * from './types';
+
+export { default as getComparisonInfo } from './getComparisonInfo';
+export { default as getComparisonFilters } from './getComparisonFilters';
diff --git a/superset-frontend/packages/superset-ui-core/src/validator/index.ts b/superset-frontend/packages/superset-ui-core/src/time-comparison/types.ts
similarity index 62%
copy from superset-frontend/packages/superset-ui-core/src/validator/index.ts
copy to superset-frontend/packages/superset-ui-core/src/time-comparison/types.ts
index 6294bddec7..d9d61a19cd 100644
--- a/superset-frontend/packages/superset-ui-core/src/validator/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/time-comparison/types.ts
@@ -17,10 +17,14 @@
* under the License.
*/
-export { default as legacyValidateInteger } from './legacyValidateInteger';
-export { default as legacyValidateNumber } from './legacyValidateNumber';
-export { default as validateInteger } from './validateInteger';
-export { default as validateNumber } from './validateNumber';
-export { default as validateNonEmpty } from './validateNonEmpty';
-export { default as validateMaxValue } from './validateMaxValue';
-export { default as validateMapboxStylesUrl } from './validateMapboxStylesUrl';
+/**
+ * Supported comparison time ranges
+ */
+
+export enum ComparisonTimeRangeType {
+ Custom = 'c',
+ InheritedRange = 'r',
+ Month = 'm',
+ Week = 'w',
+ Year = 'y',
+}
diff --git a/superset-frontend/packages/superset-ui-core/src/validator/index.ts b/superset-frontend/packages/superset-ui-core/src/validator/index.ts
index 6294bddec7..1198c4e0a5 100644
--- a/superset-frontend/packages/superset-ui-core/src/validator/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/validator/index.ts
@@ -24,3 +24,4 @@ export { default as validateNumber } from './validateNumber';
export { default as validateNonEmpty } from './validateNonEmpty';
export { default as validateMaxValue } from './validateMaxValue';
export { default as validateMapboxStylesUrl } from './validateMapboxStylesUrl';
+export { default as validateTimeComparisonRangeValues } from './validateTimeComparisonRangeValues';
diff --git a/superset-frontend/packages/superset-ui-core/src/validator/index.ts b/superset-frontend/packages/superset-ui-core/src/validator/validateTimeComparisonRangeValues.ts
similarity index 57%
copy from superset-frontend/packages/superset-ui-core/src/validator/index.ts
copy to superset-frontend/packages/superset-ui-core/src/validator/validateTimeComparisonRangeValues.ts
index 6294bddec7..c639ec6caf 100644
--- a/superset-frontend/packages/superset-ui-core/src/validator/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/validator/validateTimeComparisonRangeValues.ts
@@ -17,10 +17,21 @@
* under the License.
*/
-export { default as legacyValidateInteger } from './legacyValidateInteger';
-export { default as legacyValidateNumber } from './legacyValidateNumber';
-export { default as validateInteger } from './validateInteger';
-export { default as validateNumber } from './validateNumber';
-export { default as validateNonEmpty } from './validateNonEmpty';
-export { default as validateMaxValue } from './validateMaxValue';
-export { default as validateMapboxStylesUrl } from './validateMapboxStylesUrl';
+import { ComparisonTimeRangeType } from '../time-comparison';
+import { t } from '../translation';
+import { ensureIsArray } from '../utils';
+
+export const validateTimeComparisonRangeValues = (
+ timeRangeValue?: any,
+ controlValue?: any,
+) => {
+ const isCustomTimeRange = timeRangeValue === ComparisonTimeRangeType.Custom;
+ const isCustomControlEmpty = controlValue?.every(
+ (val: any) => ensureIsArray(val).length === 0,
+ );
+ return isCustomTimeRange && isCustomControlEmpty
+ ? [t('Filters for comparison must have a value')]
+ : [];
+};
+
+export default validateTimeComparisonRangeValues;
diff --git a/superset-frontend/packages/superset-ui-core/test/time-comparison/getComparisonFilters.test.ts b/superset-frontend/packages/superset-ui-core/test/time-comparison/getComparisonFilters.test.ts
new file mode 100644
index 0000000000..449fe5c492
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-core/test/time-comparison/getComparisonFilters.test.ts
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { getComparisonFilters } from '@superset-ui/core';
+
+const form_data = {
+ datasource: '22__table',
+ viz_type: 'pop_kpi',
+ slice_id: 97,
+ url_params: {
+ form_data_key:
+ 'TaBakyDiAx2VsQ47gLmlsJKeN4foqnoxUKdbQrM05qnKMRjO9PDe42iZN1oxmxZ8',
+ save_action: 'overwrite',
+ slice_id: '97',
+ },
+ metrics: ['count'],
+ adhoc_filters: [
+ {
+ clause: 'WHERE',
+ comparator: '2004-02-16 : 2024-02-16',
+ datasourceWarning: false,
+ expressionType: 'SIMPLE',
+ filterOptionName: 'filter_8274fo9pogn_ihi8x28o7a',
+ isExtra: false,
+ isNew: false,
+ operator: 'TEMPORAL_RANGE',
+ sqlExpression: null,
+ subject: 'order_date',
+ } as any,
+ ],
+ time_comparison: 'y',
+ adhoc_custom: [
+ {
+ clause: 'WHERE',
+ comparator: 'No filter',
+ expressionType: 'SIMPLE',
+ operator: 'TEMPORAL_RANGE',
+ subject: 'order_date',
+ },
+ ],
+ row_limit: 10000,
+ y_axis_format: 'SMART_NUMBER',
+ header_font_size: 60,
+ subheader_font_size: 26,
+ comparison_color_enabled: true,
+ extra_form_data: {},
+ force: false,
+ result_format: 'json',
+ result_type: 'full',
+};
+
+const mockExtraFormData = {
+ time_range: 'new and cool range from extra form data',
+};
+
+describe('getComparisonFilters', () => {
+ it('Keeps the original adhoc_filters since no extra data was passed', () => {
+ const result = getComparisonFilters(form_data, {});
+
+ expect(result).toEqual(form_data.adhoc_filters);
+ });
+
+ it('Updates the time_range if the filter if extra form data is passed', () => {
+ const result = getComparisonFilters(form_data, mockExtraFormData);
+
+ const expectedFilters = [
+ {
+ clause: 'WHERE',
+ comparator: 'new and cool range from extra form data',
+ datasourceWarning: false,
+ expressionType: 'SIMPLE',
+ filterOptionName: 'filter_8274fo9pogn_ihi8x28o7a',
+ isExtra: false,
+ isNew: false,
+ operator: 'TEMPORAL_RANGE',
+ sqlExpression: null,
+ subject: 'order_date',
+ } as any,
+ ];
+
+ expect(result.length).toEqual(1);
+ expect(result[0]).toEqual(expectedFilters[0]);
+ });
+
+ it('handles no time range filters', () => {
+ const result = getComparisonFilters(
+ {
+ ...form_data,
+ adhoc_filters: [
+ {
+ expressionType: 'SIMPLE',
+ subject: 'address_line1',
+ operator: 'IN',
+ comparator: ['7734 Strong St.'],
+ clause: 'WHERE',
+ isExtra: false,
+ },
+ ],
+ },
+ {},
+ );
+
+ const expectedFilters = [
+ {
+ expressionType: 'SIMPLE',
+ subject: 'address_line1',
+ operator: 'IN',
+ comparator: ['7734 Strong St.'],
+ clause: 'WHERE',
+ isExtra: false,
+ },
+ ];
+ expect(result.length).toEqual(1);
+ expect(result[0]).toEqual(expectedFilters[0]);
+ });
+
+ it('If adhoc_filter is undefrined the code wont break', () => {
+ const result = getComparisonFilters(
+ {
+ ...form_data,
+ adhoc_filters: undefined,
+ },
+ {},
+ );
+
+ expect(result).toEqual([]);
+ });
+});
diff --git a/superset-frontend/packages/superset-ui-core/test/time-comparison/getComparisonInfo.test.ts b/superset-frontend/packages/superset-ui-core/test/time-comparison/getComparisonInfo.test.ts
new file mode 100644
index 0000000000..1af9cc9e4e
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-core/test/time-comparison/getComparisonInfo.test.ts
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { getComparisonInfo, ComparisonTimeRangeType } from '@superset-ui/core';
+
+const form_data = {
+ datasource: '22__table',
+ viz_type: 'pop_kpi',
+ slice_id: 97,
+ url_params: {
+ form_data_key:
+ 'TaBakyDiAx2VsQ47gLmlsJKeN4foqnoxUKdbQrM05qnKMRjO9PDe42iZN1oxmxZ8',
+ save_action: 'overwrite',
+ slice_id: '97',
+ },
+ metrics: ['count'],
+ adhoc_filters: [
+ {
+ clause: 'WHERE',
+ comparator: '2004-02-16 : 2024-02-16',
+ datasourceWarning: false,
+ expressionType: 'SIMPLE',
+ filterOptionName: 'filter_8274fo9pogn_ihi8x28o7a',
+ isExtra: false,
+ isNew: false,
+ operator: 'TEMPORAL_RANGE',
+ sqlExpression: null,
+ subject: 'order_date',
+ } as any,
+ ],
+ time_comparison: 'y',
+ adhoc_custom: [
+ {
+ clause: 'WHERE',
+ comparator: 'No filter',
+ expressionType: 'SIMPLE',
+ operator: 'TEMPORAL_RANGE',
+ subject: 'order_date',
+ },
+ ],
+ row_limit: 10000,
+ y_axis_format: 'SMART_NUMBER',
+ header_font_size: 60,
+ subheader_font_size: 26,
+ comparison_color_enabled: true,
+ extra_form_data: {},
+ force: false,
+ result_format: 'json',
+ result_type: 'full',
+};
+
+const mockExtraFormData = {
+ time_range: 'new and cool range from extra form data',
+};
+
+describe('getComparisonInfo', () => {
+ it('Keeps the original adhoc_filters since no extra data was passed', () => {
+ const resultFormData = getComparisonInfo(
+ form_data,
+ ComparisonTimeRangeType.Year,
+ {},
+ );
+ expect(resultFormData).toEqual(form_data);
+ });
+
+ it('Updates the time_range of the adhoc_filters when extra form data is passed', () => {
+ const resultFormData = getComparisonInfo(
+ form_data,
+ ComparisonTimeRangeType.Month,
+ mockExtraFormData,
+ );
+
+ const expectedFilters = [
+ {
+ clause: 'WHERE',
+ comparator: 'new and cool range from extra form data',
+ datasourceWarning: false,
+ expressionType: 'SIMPLE',
+ filterOptionName: 'filter_8274fo9pogn_ihi8x28o7a',
+ isExtra: false,
+ isNew: false,
+ operator: 'TEMPORAL_RANGE',
+ sqlExpression: null,
+ subject: 'order_date',
+ } as any,
+ ];
+
+ expect(resultFormData.adhoc_filters?.length).toEqual(1);
+ expect(resultFormData.adhoc_filters).toEqual(expectedFilters);
+ });
+
+ it('handles no time range filters', () => {
+ const resultFormData = getComparisonInfo(
+ {
+ ...form_data,
+ adhoc_filters: [
+ {
+ expressionType: 'SIMPLE',
+ subject: 'address_line1',
+ operator: 'IN',
+ comparator: ['7734 Strong St.'],
+ clause: 'WHERE',
+ isExtra: false,
+ },
+ ],
+ },
+ ComparisonTimeRangeType.Week,
+ {},
+ );
+
+ const expectedFilters = [
+ {
+ expressionType: 'SIMPLE',
+ subject: 'address_line1',
+ operator: 'IN',
+ comparator: ['7734 Strong St.'],
+ clause: 'WHERE',
+ isExtra: false,
+ },
+ ];
+ expect(resultFormData.adhoc_filters?.length).toEqual(1);
+ expect(resultFormData.adhoc_filters?.[0]).toEqual(expectedFilters[0]);
+ });
+
+ it('If adhoc_filter is undefrined the code wont break', () => {
+ const resultFormData = getComparisonInfo(
+ {
+ ...form_data,
+ adhoc_filters: undefined,
+ },
+ ComparisonTimeRangeType.InheritedRange,
+ {},
+ );
+
+ expect(resultFormData.adhoc_filters?.length).toEqual(0);
+ expect(resultFormData.adhoc_filters).toEqual([]);
+ });
+
+ it('Handles the custom time filters and return the correct time shift text', () => {
+ const resultFormData = getComparisonInfo(
+ form_data,
+ ComparisonTimeRangeType.Custom,
+ {},
+ );
+
+ const expectedFilters = [
+ {
+ clause: 'WHERE',
+ comparator: 'No filter',
+ expressionType: 'SIMPLE',
+ operator: 'TEMPORAL_RANGE',
+ subject: 'order_date',
+ },
+ ];
+ expect(resultFormData.adhoc_filters?.length).toEqual(1);
+ expect(resultFormData.adhoc_filters).toEqual(expectedFilters);
+ });
+});
diff --git a/superset-frontend/packages/superset-ui-core/src/validator/index.ts b/superset-frontend/packages/superset-ui-core/test/time-comparison/index.test.ts
similarity index 62%
copy from superset-frontend/packages/superset-ui-core/src/validator/index.ts
copy to superset-frontend/packages/superset-ui-core/test/time-comparison/index.test.ts
index 6294bddec7..9ff8a31ab7 100644
--- a/superset-frontend/packages/superset-ui-core/src/validator/index.ts
+++ b/superset-frontend/packages/superset-ui-core/test/time-comparison/index.test.ts
@@ -17,10 +17,16 @@
* under the License.
*/
-export { default as legacyValidateInteger } from './legacyValidateInteger';
-export { default as legacyValidateNumber } from './legacyValidateNumber';
-export { default as validateInteger } from './validateInteger';
-export { default as validateNumber } from './validateNumber';
-export { default as validateNonEmpty } from './validateNonEmpty';
-export { default as validateMaxValue } from './validateMaxValue';
-export { default as validateMapboxStylesUrl } from './validateMapboxStylesUrl';
+import {
+ ComparisonTimeRangeType,
+ getComparisonFilters,
+ getComparisonInfo,
+} from '@superset-ui/core';
+
+describe('index', () => {
+ it('exports modules', () => {
+ [ComparisonTimeRangeType, getComparisonFilters, getComparisonInfo].forEach(
+ x => expect(x).toBeDefined(),
+ );
+ });
+});
diff --git a/superset-frontend/packages/superset-ui-core/test/validator/validateTimeComparisonRangeValues.test.ts b/superset-frontend/packages/superset-ui-core/test/validator/validateTimeComparisonRangeValues.test.ts
new file mode 100644
index 0000000000..ac0d5a481b
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-core/test/validator/validateTimeComparisonRangeValues.test.ts
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+ ComparisonTimeRangeType,
+ validateTimeComparisonRangeValues,
+} from '@superset-ui/core';
+import './setup';
+
+describe('validateTimeComparisonRangeValues()', () => {
+ it('returns the warning message if invalid', () => {
+ expect(
+ validateTimeComparisonRangeValues(ComparisonTimeRangeType.Custom, []),
+ ).toBeTruthy();
+ expect(
+ validateTimeComparisonRangeValues(
+ ComparisonTimeRangeType.Custom,
+ undefined,
+ ),
+ ).toBeTruthy();
+ expect(
+ validateTimeComparisonRangeValues(ComparisonTimeRangeType.Custom, null),
+ ).toBeTruthy();
+ });
+ it('returns empty array if the input is valid', () => {
+ expect(
+ validateTimeComparisonRangeValues(ComparisonTimeRangeType.Year, []),
+ ).toEqual([]);
+ expect(
+ validateTimeComparisonRangeValues(
+ ComparisonTimeRangeType.Year,
+ undefined,
+ ),
+ ).toEqual([]);
+ expect(
+ validateTimeComparisonRangeValues(ComparisonTimeRangeType.Year, null),
+ ).toEqual([]);
+ expect(
+ validateTimeComparisonRangeValues(ComparisonTimeRangeType.Custom, [1]),
+ ).toEqual([]);
+ });
+});
diff --git a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts
index 63ea2cb78c..641f9e3858 100644
--- a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts
@@ -17,11 +17,11 @@
* under the License.
*/
import {
- AdhocFilter,
buildQueryContext,
+ getComparisonInfo,
+ ComparisonTimeRangeType,
QueryFormData,
} from '@superset-ui/core';
-import { computeQueryBComparator } from '../utils';
/**
* The buildQuery function is used to create an instance of QueryContext that's
@@ -52,63 +52,28 @@ export default function buildQuery(formData: QueryFormData) {
},
]);
- const timeFilterIndex: number =
- formData.adhoc_filters?.findIndex(
- filter => 'operator' in filter && filter.operator === 'TEMPORAL_RANGE',
- ) ?? -1;
+ const comparisonFormData = getComparisonInfo(
+ formData,
+ timeComparison,
+ extraFormData,
+ );
- const timeFilter: AdhocFilter | null =
- timeFilterIndex !== -1 && formData.adhoc_filters
- ? formData.adhoc_filters[timeFilterIndex]
- : null;
-
- let formDataB: QueryFormData;
- let queryBComparator = null;
-
- if (timeComparison !== 'c') {
- queryBComparator = computeQueryBComparator(
- formData.adhoc_filters || [],
- timeComparison,
- extraFormData,
- );
-
- const queryBFilter: any = {
- ...timeFilter,
- comparator: queryBComparator,
- };
-
- const otherFilters = formData.adhoc_filters?.filter(
- (_value: any, index: number) => timeFilterIndex !== index,
- );
- const queryBFilters = otherFilters
- ? [queryBFilter, ...otherFilters]
- : [queryBFilter];
-
- formDataB = {
- ...formData,
- adhoc_filters: queryBFilters,
- extra_form_data: {
- ...extraFormData,
- time_range: undefined,
- },
- };
- } else {
- formDataB = {
- ...formData,
- adhoc_filters: formData.adhoc_custom,
- extra_form_data: {
- ...extraFormData,
- time_range: undefined,
+ const queryContextB = buildQueryContext(
+ comparisonFormData,
+ baseQueryObject => [
+ {
+ ...baseQueryObject,
+ groupby,
+ extras: {
+ ...baseQueryObject.extras,
+ instant_time_comparison_range:
+ timeComparison !== ComparisonTimeRangeType.Custom
+ ? timeComparison
+ : undefined,
+ },
},
- };
- }
-
- const queryContextB = buildQueryContext(formDataB, baseQueryObject => [
- {
- ...baseQueryObject,
- groupby,
- },
- ]);
+ ],
+ );
return {
...queryContextA,
diff --git a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/controlPanel.ts b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/controlPanel.ts
index d70be63125..1f01a8a4b7 100644
--- a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/controlPanel.ts
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/controlPanel.ts
@@ -16,7 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { ensureIsArray, t } from '@superset-ui/core';
+import {
+ ComparisonTimeRangeType,
+ t,
+ validateTimeComparisonRangeValues,
+} from '@superset-ui/core';
import {
ControlPanelConfig,
ControlPanelState,
@@ -25,19 +29,6 @@ import {
sharedControls,
} from '@superset-ui/chart-controls';
-const validateTimeComparisonRangeValues = (
- timeRangeValue?: any,
- controlValue?: any,
-) => {
- const isCustomTimeRange = timeRangeValue === 'c';
- const isCustomControlEmpty = controlValue?.every(
- (val: any) => ensureIsArray(val).length === 0,
- );
- return isCustomTimeRange && isCustomControlEmpty
- ? [t('Filters for comparison must have a value')]
- : [];
-};
-
const config: ControlPanelConfig = {
controlPanelSections: [
{
@@ -79,7 +70,8 @@ const config: ControlPanelConfig = {
description:
'This only applies when selecting the Range for Comparison Type: Custom',
visibility: ({ controls }) =>
- controls?.time_comparison?.value === 'c',
+ controls?.time_comparison?.value ===
+ ComparisonTimeRangeType.Custom,
mapStateToProps: (
state: ControlPanelState,
controlState: ControlState,
diff --git a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/transformProps.ts b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/transformProps.ts
index fb82f40928..5e49950410 100644
--- a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/transformProps.ts
@@ -23,8 +23,8 @@ import {
getValueFormatter,
NumberFormats,
getNumberFormatter,
+ formatTimeRange,
} from '@superset-ui/core';
-import { computeQueryBComparator, formatCustomComparator } from '../utils';
export const parseMetricValue = (metricValue: number | string | null) => {
if (typeof metricValue === 'string') {
@@ -85,7 +85,11 @@ export default function transformProps(chartProps: ChartProps) {
comparisonColorEnabled,
} = formData;
const { data: dataA = [] } = queriesData[0];
- const { data: dataB = [] } = queriesData[1];
+ const {
+ data: dataB = [],
+ from_dttm: comparisonFromDatetime,
+ to_dttm: comparisonToDatetime,
+ } = queriesData[1];
const data = dataA;
const metricName = getMetricLabel(metric);
let bigNumber: number | string =
@@ -129,18 +133,10 @@ export default function transformProps(chartProps: ChartProps) {
prevNumber = numberFormatter(prevNumber);
valueDifference = numberFormatter(valueDifference);
const percentDifference: string = formatPercentChange(percentDifferenceNum);
- const comparatorText =
- formData.timeComparison !== 'c'
- ? ` ${computeQueryBComparator(
- formData.adhocFilters,
- formData.timeComparison,
- formData.extraFormData,
- ' - ',
- )}`
- : `${formatCustomComparator(
- formData.adhocCustom,
- formData.extraFormData,
- )}`;
+ const comparatorText = formatTimeRange('%Y-%m-%d', [
+ comparisonFromDatetime,
+ comparisonToDatetime,
+ ]);
return {
width,
diff --git a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/utils.ts b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/utils.ts
deleted file mode 100644
index eda69c2bb2..0000000000
--- a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/utils.ts
+++ /dev/null
@@ -1,277 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { AdhocFilter } from '@superset-ui/core';
-import moment, { Moment } from 'moment';
-
-type MomentTuple = [moment.Moment | null, moment.Moment | null];
-
-const getSinceUntil = (
- timeRange: string | null = null,
- relativeStart: string | null = null,
- relativeEnd: string | null = null,
-): MomentTuple => {
- const separator = ' : ';
- const effectiveRelativeStart = relativeStart || 'today';
- const effectiveRelativeEnd = relativeEnd || 'today';
-
- if (!timeRange) {
- return [null, null];
- }
-
- let modTimeRange: string | null = timeRange;
-
- if (timeRange === 'NO_TIME_RANGE' || timeRange === '_(NO_TIME_RANGE)') {
- return [null, null];
- }
-
- if (timeRange?.startsWith('last') && !timeRange.includes(separator)) {
- modTimeRange = timeRange + separator + effectiveRelativeEnd;
- }
-
- if (timeRange?.startsWith('next') && !timeRange.includes(separator)) {
- modTimeRange = effectiveRelativeStart + separator + timeRange;
- }
-
- if (
- timeRange?.startsWith('previous calendar week') &&
- !timeRange.includes(separator)
- ) {
- return [
- moment().subtract(1, 'week').startOf('week'),
- moment().startOf('week'),
- ];
- }
-
- if (
- timeRange?.startsWith('previous calendar month') &&
- !timeRange.includes(separator)
- ) {
- return [
- moment().subtract(1, 'month').startOf('month'),
- moment().startOf('month'),
- ];
- }
-
- if (
- timeRange?.startsWith('previous calendar year') &&
- !timeRange.includes(separator)
- ) {
- return [
- moment().subtract(1, 'year').startOf('year'),
- moment().startOf('year'),
- ];
- }
-
- const timeRangeLookup: Array<[RegExp, (...args: string[]) => Moment]> = [
- [
- /^last\s+(day|week|month|quarter|year)$/i,
- (unit: string) =>
- moment().subtract(1, unit as moment.unitOfTime.DurationConstructor),
- ],
- [
- /^last\s+([0-9]+)\s+(second|minute|hour|day|week|month|year)s?$/i,
- (delta: string, unit: string) =>
- moment().subtract(delta, unit as moment.unitOfTime.DurationConstructor),
- ],
- [
- /^next\s+([0-9]+)\s+(second|minute|hour|day|week|month|year)s?$/i,
- (delta: string, unit: string) =>
- moment().add(delta, unit as moment.unitOfTime.DurationConstructor),
- ],
- [
- // eslint-disable-next-line no-useless-escape
- /DATEADD\(DATETIME\("([^"]+)"\),\s*(-?\d+),\s*([^\)]+)\)/i,
- (timePart: string, delta: string, unit: string) => {
- if (timePart === 'now') {
- return moment().add(
- delta,
- unit as moment.unitOfTime.DurationConstructor,
- );
- }
- if (moment(timePart.toUpperCase(), true).isValid()) {
- return moment(timePart).add(
- delta,
- unit as moment.unitOfTime.DurationConstructor,
- );
- }
- return moment();
- },
- ],
- ];
-
- const sinceAndUntilPartition = modTimeRange
- .split(separator, 2)
- .map(part => part.trim());
-
- const sinceAndUntil: (Moment | null)[] = sinceAndUntilPartition.map(part => {
- if (!part) {
- return null;
- }
-
- let transformedValue: Moment | null = null;
- // Matching time_range_lookup
- const matched = timeRangeLookup.some(([pattern, fn]) => {
- const result = part.match(pattern);
- if (result) {
- transformedValue = fn(...result.slice(1));
- return true;
- }
-
- if (part === 'today') {
- transformedValue = moment().startOf('day');
- return true;
- }
-
- if (part === 'now') {
- transformedValue = moment();
- return true;
- }
- return false;
- });
-
- if (matched && transformedValue !== null) {
- // Handle the transformed value
- } else {
- // Handle the case when there was no match
- transformedValue = moment(`${part}`);
- }
-
- return transformedValue;
- });
-
- const [_since, _until] = sinceAndUntil;
-
- if (_since && _until && _since.isAfter(_until)) {
- throw new Error('From date cannot be larger than to date');
- }
-
- return [_since, _until];
-};
-
-const calculatePrev = (
- startDate: Moment | null,
- endDate: Moment | null,
- calcType: String,
-) => {
- if (!startDate || !endDate) {
- return [null, null];
- }
-
- const daysBetween = endDate.diff(startDate, 'days');
-
- let startDatePrev = moment();
- let endDatePrev = moment();
- if (calcType === 'y') {
- startDatePrev = startDate.subtract(1, 'year');
- endDatePrev = endDate.subtract(1, 'year');
- } else if (calcType === 'w') {
- startDatePrev = startDate.subtract(1, 'week');
- endDatePrev = endDate.subtract(1, 'week');
- } else if (calcType === 'm') {
- startDatePrev = startDate.subtract(1, 'month');
- endDatePrev = endDate.subtract(1, 'month');
- } else if (calcType === 'r') {
- startDatePrev = startDate.clone().subtract(daysBetween.valueOf(), 'day');
- endDatePrev = startDate;
- } else {
- startDatePrev = startDate.subtract(1, 'year');
- endDatePrev = endDate.subtract(1, 'year');
- }
-
- return [startDatePrev, endDatePrev];
-};
-
-const getTimeRange = (
- adhocFilters: AdhocFilter[],
- extraFormData: any,
-): string | null => {
- const timeFilterIndex =
- adhocFilters?.findIndex(
- filter => 'operator' in filter && filter.operator === 'TEMPORAL_RANGE',
- ) ?? -1;
-
- const timeFilter =
- timeFilterIndex !== -1 ? adhocFilters[timeFilterIndex] : null;
-
- if (
- timeFilter &&
- 'comparator' in timeFilter &&
- typeof timeFilter.comparator === 'string'
- ) {
- let timeRange = timeFilter.comparator.toLocaleLowerCase();
- if (extraFormData?.time_range) {
- timeRange = extraFormData.time_range;
- }
- return timeRange;
- }
-
- return null;
-};
-
-export const computeQueryBComparator = (
- adhocFilters: AdhocFilter[],
- timeComparison: string,
- extraFormData: any,
- join = ':',
-) => {
- const timeRange = getTimeRange(adhocFilters, extraFormData);
-
- let testSince = null;
- let testUntil = null;
-
- if (timeRange) {
- [testSince, testUntil] = getSinceUntil(timeRange);
- }
-
- if (timeComparison !== 'c') {
- const [prevStartDateMoment, prevEndDateMoment] = calculatePrev(
- testSince,
- testUntil,
- timeComparison,
- );
-
- return `${prevStartDateMoment?.format(
- 'YYYY-MM-DDTHH:mm:ss',
- )} ${join} ${prevEndDateMoment?.format('YYYY-MM-DDTHH:mm:ss')}`.replace(
- /Z/g,
- '',
- );
- }
-
- return null;
-};
-
-export const formatCustomComparator = (
- adhocFilters: AdhocFilter[],
- extraFormData: any,
-): string => {
- const timeRange = getTimeRange(adhocFilters, extraFormData);
-
- if (timeRange) {
- const [start, end] = timeRange.split(' : ').map(dateStr => {
- const formattedDate = moment(dateStr).format('YYYY-MM-DDTHH:mm:ss');
- return formattedDate.replace(/Z/g, '');
- });
-
- return `${start} - ${end}`;
- }
-
- return '';
-};
diff --git a/superset/charts/schemas.py b/superset/charts/schemas.py
index 48e0cbb318..611f7af597 100644
--- a/superset/charts/schemas.py
+++ b/superset/charts/schemas.py
@@ -990,6 +990,14 @@ class ChartDataExtrasSchema(Schema):
),
allow_none=True,
)
+ instant_time_comparison_range = fields.String(
+ metadata={
+ "description": "This is only set using the new time comparison controls "
+ "that is made available in some plugins behind the experimental "
+ "feature flag."
+ },
+ allow_none=True,
+ )
class AnnotationLayerSchema(Schema):
diff --git a/superset/common/utils/time_range_utils.py b/superset/common/utils/time_range_utils.py
index 5f9139c047..2ceb9f766e 100644
--- a/superset/common/utils/time_range_utils.py
+++ b/superset/common/utils/time_range_utils.py
@@ -39,6 +39,9 @@ def get_since_until_from_time_range(
),
time_range=time_range,
time_shift=time_shift,
+ instant_time_comparison_range=(extras or {}).get(
+ "instant_time_comparison_range"
+ ),
)
diff --git a/superset/constants.py b/superset/constants.py
index 4f01674bd4..bf4e7717d5 100644
--- a/superset/constants.py
+++ b/superset/constants.py
@@ -42,6 +42,14 @@ QUERY_EARLY_CANCEL_KEY = "early_cancel_query"
LRU_CACHE_MAX_SIZE = 256
+# Used when calculating the time shift for time comparison
+class InstantTimeComparison(StrEnum):
+ INHERITED = "r"
+ YEAR = "y"
+ MONTH = "m"
+ WEEK = "w"
+
+
class RouteMethod: # pylint: disable=too-few-public-methods
"""
Route methods are a FAB concept around ModelView and RestModelView
diff --git a/superset/utils/date_parser.py b/superset/utils/date_parser.py
index 2d49424a82..0253edee1c 100644
--- a/superset/utils/date_parser.py
+++ b/superset/utils/date_parser.py
@@ -46,7 +46,7 @@ from superset.commands.chart.exceptions import (
TimeRangeAmbiguousError,
TimeRangeParseFailError,
)
-from superset.constants import LRU_CACHE_MAX_SIZE, NO_TIME_RANGE
+from superset.constants import InstantTimeComparison, LRU_CACHE_MAX_SIZE, NO_TIME_RANGE
ParserElement.enablePackrat()
@@ -142,13 +142,14 @@ def parse_past_timedelta(
)
-def get_since_until( # pylint: disable=too-many-arguments,too-many-locals,too-many-branches
+def get_since_until( # pylint: disable=too-many-arguments,too-many-locals,too-many-branches,too-many-statements
time_range: Optional[str] = None,
since: Optional[str] = None,
until: Optional[str] = None,
time_shift: Optional[str] = None,
relative_start: Optional[str] = None,
relative_end: Optional[str] = None,
+ instant_time_comparison_range: Optional[str] = None,
) -> tuple[Optional[datetime], Optional[datetime]]:
"""Return `since` and `until` date time tuple from string representations of
time_range, since, until and time_shift.
@@ -263,6 +264,47 @@ def get_since_until( # pylint: disable=too-many-arguments,too-many-locals,too-m
_since = _since if _since is None else (_since - time_delta)
_until = _until if _until is None else (_until - time_delta)
+ if instant_time_comparison_range:
+ # This is only set using the new time comparison controls
+ # that is made available in some plugins behind the experimental
+ # feature flag.
+ # pylint: disable=import-outside-toplevel
+ from superset import feature_flag_manager
+
+ if feature_flag_manager.is_feature_enabled("CHART_PLUGINS_EXPERIMENTAL"):
+ time_unit = ""
+ delta_in_days = None
+ if instant_time_comparison_range == InstantTimeComparison.YEAR:
+ time_unit = "YEAR"
+ elif instant_time_comparison_range == InstantTimeComparison.MONTH:
+ time_unit = "MONTH"
+ elif instant_time_comparison_range == InstantTimeComparison.WEEK:
+ time_unit = "WEEK"
+ elif instant_time_comparison_range == InstantTimeComparison.INHERITED:
+ delta_in_days = (_until - _since).days if _since and _until else None
+ time_unit = "DAY"
+
+ if time_unit:
+ strtfime_since = (
+ _since.strftime("%Y-%m-%dT%H:%M:%S") if _since else relative_start
+ )
+ strtfime_until = (
+ _until.strftime("%Y-%m-%dT%H:%M:%S") if _until else relative_end
+ )
+
+ since_and_until = [
+ (
+ f"DATEADD(DATETIME('{strtfime_since}'), "
+ f"-{delta_in_days or 1}, {time_unit})"
+ ),
+ (
+ f"DATEADD(DATETIME('{strtfime_until}'), "
+ f"-{delta_in_days or 1}, {time_unit})"
+ ),
+ ]
+
+ _since, _until = map(datetime_eval, since_and_until)
+
if _since and _until and _since > _until:
raise ValueError(_("From date cannot be larger than to date"))
diff --git a/tests/unit_tests/utils/date_parser_tests.py b/tests/unit_tests/utils/date_parser_tests.py
index 0311377237..41f4c95022 100644
--- a/tests/unit_tests/utils/date_parser_tests.py
+++ b/tests/unit_tests/utils/date_parser_tests.py
@@ -35,6 +35,7 @@ from superset.utils.date_parser import (
parse_human_timedelta,
parse_past_timedelta,
)
+from tests.unit_tests.conftest import with_feature_flags
def mock_parse_human_datetime(s: str) -> Optional[datetime]:
@@ -157,10 +158,81 @@ def test_get_since_until() -> None:
expected = datetime(2015, 1, 1, 0, 0, 0), datetime(2016, 1, 1, 0, 0, 0)
assert result == expected
+ # Tests for our new instant_time_comparison logic and Feature Flag off
+ result = get_since_until(
+ time_range="2000-01-01T00:00:00 : 2018-01-01T00:00:00",
+ instant_time_comparison_range="y",
+ )
+ expected = datetime(2000, 1, 1), datetime(2018, 1, 1)
+ assert result == expected
+
+ result = get_since_until(
+ time_range="2000-01-01T00:00:00 : 2018-01-01T00:00:00",
+ instant_time_comparison_range="m",
+ )
+ expected = datetime(2000, 1, 1), datetime(2018, 1, 1)
+ assert result == expected
+
+ result = get_since_until(
+ time_range="2000-01-01T00:00:00 : 2018-01-01T00:00:00",
+ instant_time_comparison_range="w",
+ )
+ expected = datetime(2000, 1, 1), datetime(2018, 1, 1)
+ assert result == expected
+
+ result = get_since_until(
+ time_range="2000-01-01T00:00:00 : 2018-01-01T00:00:00",
+ instant_time_comparison_range="r",
+ )
+ expected = datetime(2000, 1, 1), datetime(2018, 1, 1)
+ assert result == expected
+
with pytest.raises(ValueError):
get_since_until(time_range="tomorrow : yesterday")
+@with_feature_flags(CHART_PLUGINS_EXPERIMENTAL=True)
+@patch("superset.utils.date_parser.parse_human_datetime", mock_parse_human_datetime)
+def test_get_since_until_instant_time_comparison_enabled() -> None:
+ result: tuple[Optional[datetime], Optional[datetime]]
+ expected: tuple[Optional[datetime], Optional[datetime]]
+
+ result = get_since_until(
+ time_range="2000-01-01T00:00:00 : 2018-01-01T00:00:00",
+ instant_time_comparison_range="y",
+ )
+ expected = datetime(1999, 1, 1), datetime(2017, 1, 1)
+ assert result == expected
+
+ result = get_since_until(
+ time_range="2000-01-01T00:00:00 : 2018-01-01T00:00:00",
+ instant_time_comparison_range="m",
+ )
+ expected = datetime(1999, 12, 1), datetime(2017, 12, 1)
+ assert result == expected
+
+ result = get_since_until(
+ time_range="2000-01-01T00:00:00 : 2018-01-01T00:00:00",
+ instant_time_comparison_range="w",
+ )
+ expected = datetime(1999, 12, 25), datetime(2017, 12, 25)
+ assert result == expected
+
+ result = get_since_until(
+ time_range="2000-01-01T00:00:00 : 2018-01-01T00:00:00",
+ instant_time_comparison_range="r",
+ )
+ expected = datetime(1981, 12, 31), datetime(2000, 1, 1)
+ assert result == expected
+
+ result = get_since_until(
+ time_range="2000-01-01T00:00:00 : 2018-01-01T00:00:00",
+ instant_time_comparison_range="unknown",
+ )
+ expected = datetime(2000, 1, 1), datetime(2018, 1, 1)
+ assert result == expected
+
+
@patch("superset.utils.date_parser.parse_human_datetime", mock_parse_human_datetime)
def test_datetime_eval() -> None:
result = datetime_eval("datetime('now')")