diff --git a/README.md b/README.md index e0bb2915399..c23fd3fdbe3 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ under the License. # Superset [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/license/apache-2-0) -[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/apache/superset?sort=semver)](https://github.com/apache/superset/tree/latest) +[![Latest Release on Github](https://img.shields.io/github/v/release/apache/superset?sort=semver)](https://github.com/apache/superset/releases/latest) [![Build Status](https://github.com/apache/superset/actions/workflows/superset-python-unittest.yml/badge.svg)](https://github.com/apache/superset/actions) [![PyPI version](https://badge.fury.io/py/apache-superset.svg)](https://badge.fury.io/py/apache-superset) [![Coverage Status](https://codecov.io/github/apache/superset/coverage.svg?branch=master)](https://codecov.io/github/apache/superset) diff --git a/null_byte.csv b/null_byte.csv deleted file mode 100644 index 55132aaa639..00000000000 Binary files a/null_byte.csv and /dev/null differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/package.json b/superset-frontend/plugins/plugin-chart-echarts/package.json index 64da86afdcc..01763f3078a 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/package.json +++ b/superset-frontend/plugins/plugin-chart-echarts/package.json @@ -24,10 +24,10 @@ "lib" ], "dependencies": { + "@types/react-redux": "^7.1.10", "d3-array": "^1.2.0", - "lodash": "^4.17.21", "dayjs": "^1.11.13", - "@types/react-redux": "^7.1.10" + "lodash": "^4.17.21" }, "peerDependencies": { "@superset-ui/chart-controls": "*", diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts index ea9c63c7f25..3015010d05f 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts @@ -156,9 +156,15 @@ export function sortAndFilterSeries( case SortSeriesType.Avg: aggregator = name => ({ name, value: meanBy(rows, name) }); break; - default: - aggregator = name => ({ name, value: name.toLowerCase() }); - break; + default: { + const collator = new Intl.Collator(undefined, { + numeric: true, + sensitivity: 'base', + }); + return seriesNames.sort((a, b) => + sortSeriesAscending ? collator.compare(a, b) : collator.compare(b, a), + ); + } } const sortedValues = seriesNames.map(aggregator); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts index 323f576fd09..5f54c63f94d 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts @@ -68,6 +68,39 @@ const sortData: DataRecord[] = [ { my_x_axis: null, x: 4, y: 3, z: 7 }, ]; +const sortDataWithNumbers: DataRecord[] = [ + { + my_x_axis: 'my_axis', + '9. September': 6, + 6: 1, + '11. November': 8, + 8: 2, + '10. October': 1, + 10: 4, + '3. March': 2, + '8. August': 6, + 2: 1, + 12: 3, + 9: 1, + '1. January': 1, + '4. April': 12, + '2. February': 9, + 5: 4, + 3: 1, + 11: 2, + '12. December': 4, + 1: 7, + '6. June': 1, + 4: 5, + 7: 2, + c: 0, + '7. July': 2, + d: 0, + '5. May': 4, + a: 1, + }, +]; + const totalStackedValues = [3, 15, 14]; test('sortRows by name ascending', () => { @@ -289,6 +322,84 @@ test('sortAndFilterSeries by name descending', () => { sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Name, false), ).toEqual(['z', 'y', 'x']); }); +test('sortAndFilterSeries by name with numbers asc', () => { + expect( + sortAndFilterSeries( + sortDataWithNumbers, + 'my_x_axis', + [], + SortSeriesType.Name, + true, + ), + ).toEqual([ + '1', + '1. January', + '2', + '2. February', + '3', + '3. March', + '4', + '4. April', + '5', + '5. May', + '6', + '6. June', + '7', + '7. July', + '8', + '8. August', + '9', + '9. September', + '10', + '10. October', + '11', + '11. November', + '12', + '12. December', + 'a', + 'c', + 'd', + ]); +}); +test('sortAndFilterSeries by name with numbers desc', () => { + expect( + sortAndFilterSeries( + sortDataWithNumbers, + 'my_x_axis', + [], + SortSeriesType.Name, + false, + ), + ).toEqual([ + 'd', + 'c', + 'a', + '12. December', + '12', + '11. November', + '11', + '10. October', + '10', + '9. September', + '9', + '8. August', + '8', + '7. July', + '7', + '6. June', + '6', + '5. May', + '5', + '4. April', + '4', + '3. March', + '3', + '2. February', + '2', + '1. January', + '1', + ]); +}); describe('extractSeries', () => { it('should generate a valid ECharts timeseries series object', () => { diff --git a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx index 933ee6a0c1d..ef3a8e700c2 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx @@ -486,8 +486,9 @@ const config: ControlPanelConfig = { return true; }, mapStateToProps(explore, _, chart) { - const timeComparisonStatus = - !!explore?.controls?.time_compare?.value; + const timeComparisonStatus = !isEmpty( + explore?.controls?.time_compare?.value, + ); const { colnames: _colnames, coltypes: _coltypes } = chart?.queriesResponse?.[0] ?? {}; diff --git a/superset-frontend/src/explore/components/controls/ColorPickerControl.jsx b/superset-frontend/src/explore/components/controls/ColorPickerControl.jsx index 61b3a7f0011..4cbf930ec21 100644 --- a/superset-frontend/src/explore/components/controls/ColorPickerControl.jsx +++ b/superset-frontend/src/explore/components/controls/ColorPickerControl.jsx @@ -78,7 +78,7 @@ export default class ColorPickerControl extends Component { renderPopover() { const presetColors = getCategoricalSchemeRegistry() .get() - .colors.filter((s, i) => i < 7); + .colors.filter((s, i) => i < 9); return (
None: + """ + Update the catalog of the datasets that are associated with database. + """ + from superset.connectors.sqla.models import SqlaTable + from superset.models.sql_lab import Query, SavedQuery, TableSchema, TabState + + for model in [ + SqlaTable, + Query, + SavedQuery, + TabState, + TableSchema, + ]: + fk = "db_id" if model == SavedQuery else "database_id" + predicate = {fk: database_id} + update = {"catalog": new_catalog} + db.session.query(model).filter_by(**predicate).update(update) + def validate(self) -> None: if database_name := self._properties.get("database_name"): if not DatabaseDAO.validate_update_uniqueness( diff --git a/superset/migrations/versions/2024-09-25_17-59_7b17aa722e30_uuidmixin.py b/superset/migrations/versions/2024-09-25_17-59_7b17aa722e30_uuidmixin.py index 2366d3bf261..9cc3735f28c 100644 --- a/superset/migrations/versions/2024-09-25_17-59_7b17aa722e30_uuidmixin.py +++ b/superset/migrations/versions/2024-09-25_17-59_7b17aa722e30_uuidmixin.py @@ -24,7 +24,8 @@ Create Date: 2024-09-25 17:59:21.028426 import sqlalchemy as sa import sqlalchemy_utils -from alembic import op + +from superset.migrations.shared.utils import add_columns, drop_columns # revision identifiers, used by Alembic. revision = "7b17aa722e30" @@ -32,16 +33,16 @@ down_revision = "48cbb571fa3a" def upgrade(): - op.add_column( + add_columns( "css_templates", sa.Column("uuid", sqlalchemy_utils.types.uuid.UUIDType(), nullable=True), ) - op.add_column( + add_columns( "favstar", sa.Column("uuid", sqlalchemy_utils.types.uuid.UUIDType(), nullable=True), ) def downgrade(): - op.drop_column("css_templates", "uuid") - op.drop_column("favstar", "uuid") + drop_columns("css_templates", "uuid") + drop_columns("favstar", "uuid") diff --git a/superset/migrations/versions/2025-03-03_20-52_94e7a3499973_add_folders_column_to_dataset.py b/superset/migrations/versions/2025-03-03_20-52_94e7a3499973_add_folders_column_to_dataset.py index 3e767f36a6f..e9f7d387c51 100644 --- a/superset/migrations/versions/2025-03-03_20-52_94e7a3499973_add_folders_column_to_dataset.py +++ b/superset/migrations/versions/2025-03-03_20-52_94e7a3499973_add_folders_column_to_dataset.py @@ -23,20 +23,21 @@ Create Date: 2025-03-03 20:52:24.585143 """ import sqlalchemy as sa -from alembic import op from sqlalchemy.types import JSON +from superset.migrations.shared.utils import add_columns, drop_columns + # revision identifiers, used by Alembic. revision = "94e7a3499973" down_revision = "74ad1125881c" def upgrade(): - op.add_column( + add_columns( "tables", sa.Column("folders", JSON, nullable=True), ) def downgrade(): - op.drop_column("tables", "folders") + drop_columns("tables", "folders") diff --git a/superset/security/manager.py b/superset/security/manager.py index 80a1fb957dd..d11e0f3e41c 100644 --- a/superset/security/manager.py +++ b/superset/security/manager.py @@ -1126,7 +1126,9 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods """ Find a List of models by a list of ids, if defined applies `base_filter` """ - query = self.get_session.query(Role).filter(Role.id.in_(role_ids)) + query = self.get_session.query(self.role_model).filter( + self.role_model.id.in_(role_ids) + ) return query.all() def copy_role( diff --git a/superset/translations/nl/LC_MESSAGES/messages.po b/superset/translations/nl/LC_MESSAGES/messages.po index e652b9b74f4..ffdc05c88ba 100644 --- a/superset/translations/nl/LC_MESSAGES/messages.po +++ b/superset/translations/nl/LC_MESSAGES/messages.po @@ -3056,9 +3056,8 @@ msgstr "Configuratie" msgid "Configure Advanced Time Range " msgstr "Configureer geavanceerd tijdbereik " -#, fuzzy msgid "Configure Time Range: Current..." -msgstr "Configureer Tijdbereik: Laatste..." +msgstr "Configureer Tijdbereik: Huidige..." msgid "Configure Time Range: Last..." msgstr "Configureer Tijdbereik: Laatste..." @@ -3366,29 +3365,23 @@ msgstr "Valuta voorvoegsel of achtervoegsel" msgid "Currency symbol" msgstr "Valuta symbool" -#, fuzzy msgid "Current" -msgstr "Valuta" +msgstr "Huidig" -#, fuzzy msgid "Current day" -msgstr "Valuta" +msgstr "Huidige dag" -#, fuzzy msgid "Current month" -msgstr "Valuta symbool" +msgstr "Huidige maand" -#, fuzzy msgid "Current quarter" -msgstr "Voer huidige query uit" +msgstr "Huidig kwartaal" -#, fuzzy msgid "Current week" -msgstr "Voer huidige query uit" +msgstr "Huidige week" -#, fuzzy msgid "Current year" -msgstr "Valuta" +msgstr "Huidig jaar" #, python-format msgid "Currently rendered: %s" diff --git a/tests/unit_tests/commands/dashboard/__init__.py b/tests/unit_tests/commands/dashboard/__init__.py new file mode 100644 index 00000000000..13a83393a91 --- /dev/null +++ b/tests/unit_tests/commands/dashboard/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/tests/unit_tests/commands/dashboard/create_test.py b/tests/unit_tests/commands/dashboard/create_test.py new file mode 100644 index 00000000000..9bb0cff9ab9 --- /dev/null +++ b/tests/unit_tests/commands/dashboard/create_test.py @@ -0,0 +1,98 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import pytest +from flask_appbuilder.security.sqla.models import Role, User +from pytest_mock import MockerFixture + +from superset.commands.dashboard.create import CreateDashboardCommand +from superset.extensions import security_manager +from tests.unit_tests.fixtures.common import admin_user, after_each # noqa: F401 + + +def test_validate_custom_role_class( + mocker: MockerFixture, + monkeypatch: pytest.MonkeyPatch, + admin_user: User, # noqa: F811 + after_each: None, # noqa: F811 +) -> None: + class CustomRoleModel(Role): + __tablename__ = "ab_role" + + monkeypatch.setattr(security_manager, "role_model", CustomRoleModel) + mocker.patch.object(security_manager, "is_admin", return_value=True) + + owner_ids = [admin_user.id] + role_ids = [role.id for role in admin_user.roles] + + command = CreateDashboardCommand(data={"owners": owner_ids, "roles": role_ids}) + command.validate() + + for role in command._properties["roles"]: + assert isinstance(role, CustomRoleModel) + + +def test_validate_custom_user_class( + mocker: MockerFixture, + monkeypatch: pytest.MonkeyPatch, + admin_user: User, # noqa: F811 + after_each: None, # noqa: F811 +) -> None: + class CustomUserModel(User): + __tablename__ = "ab_user" + + monkeypatch.setattr(security_manager, "user_model", CustomUserModel) + mocker.patch.object(security_manager, "is_admin", return_value=True) + + owner_ids = [admin_user.id] + role_ids = [role.id for role in admin_user.roles] + + command = CreateDashboardCommand(data={"owners": owner_ids, "roles": role_ids}) + command.validate() + + for owner in command._properties["owners"]: + assert isinstance(owner, CustomUserModel) + + +def test_validate_custom_role_and_user_class( + mocker: MockerFixture, + monkeypatch: pytest.MonkeyPatch, + admin_user: User, # noqa: F811 + after_each: None, # noqa: F811 +) -> None: + class CustomRoleModel(Role): + __tablename__ = "ab_role" + + class CustomUserModel(User): + __tablename__ = "ab_user" + + monkeypatch.setattr(security_manager, "role_model", CustomRoleModel) + monkeypatch.setattr(security_manager, "user_model", CustomUserModel) + mocker.patch.object(security_manager, "is_admin", return_value=True) + + owner_ids = [admin_user.id] + role_ids = [role.id for role in admin_user.roles] + + command = CreateDashboardCommand(data={"owners": owner_ids, "roles": role_ids}) + command.validate() + + for role in command._properties["roles"]: + assert isinstance(role, CustomRoleModel) + + for owner in command._properties["owners"]: + assert isinstance(owner, CustomUserModel) diff --git a/tests/unit_tests/commands/databases/update_test.py b/tests/unit_tests/commands/databases/update_test.py index a74ff3c5c06..5e6b0869d72 100644 --- a/tests/unit_tests/commands/databases/update_test.py +++ b/tests/unit_tests/commands/databases/update_test.py @@ -580,3 +580,65 @@ def test_update_other_fields_dont_affect_oauth( add_pvm.assert_not_called() database_needs_oauth2.purge_oauth2_tokens.assert_not_called() + + +def test_update_with_catalog_change(mocker: MockerFixture) -> None: + """ + Test that assets are updated when the main catalog changes. + """ + old_database = mocker.MagicMock(allow_multi_catalog=False) + old_database.get_default_catalog.return_value = "project-A" + old_database.id = 1 + + new_database = mocker.MagicMock(allow_multi_catalog=False) + new_database.get_default_catalog.return_value = "project-B" + + database_dao = mocker.patch("superset.commands.database.update.DatabaseDAO") + database_dao.find_by_id.return_value = old_database + database_dao.update.return_value = new_database + + mocker.patch("superset.commands.database.update.SyncPermissionsCommand") + mocker.patch.object( + UpdateDatabaseCommand, + "validate", + ) + update_catalog_attribute = mocker.patch.object( + UpdateDatabaseCommand, + "_update_catalog_attribute", + ) + + UpdateDatabaseCommand(1, {}).run() + + update_catalog_attribute.assert_called_once_with(1, "project-B") + + +def test_update_without_catalog_change(mocker: MockerFixture) -> None: + """ + Test that assets are not updated when the main catalog doesn't change. + """ + old_database = mocker.MagicMock(allow_multi_catalog=False) + old_database.database_name = "Ye Old DB" + old_database.get_default_catalog.return_value = "project-A" + old_database.id = 1 + + new_database = mocker.MagicMock(allow_multi_catalog=False) + new_database.database_name = "Fancy new DB" + new_database.get_default_catalog.return_value = "project-A" + + database_dao = mocker.patch("superset.commands.database.update.DatabaseDAO") + database_dao.find_by_id.return_value = old_database + database_dao.update.return_value = new_database + + mocker.patch("superset.commands.database.update.SyncPermissionsCommand") + mocker.patch.object( + UpdateDatabaseCommand, + "validate", + ) + update_catalog_attribute = mocker.patch.object( + UpdateDatabaseCommand, + "_update_catalog_attribute", + ) + + UpdateDatabaseCommand(1, {}).run() + + update_catalog_attribute.assert_not_called()