diff --git a/api/controllers/console/explore/recommended_app.py b/api/controllers/console/explore/recommended_app.py index 1125c0bc580..1fdf61f049d 100644 --- a/api/controllers/console/explore/recommended_app.py +++ b/api/controllers/console/explore/recommended_app.py @@ -7,17 +7,17 @@ from pydantic import BaseModel, Field, computed_field, field_validator from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.explore.error import RecommendedAppNotFoundError -from controllers.console.wraps import account_initialization_required, model_validate, with_current_user +from controllers.console.flask_admission import console_account_admission +from controllers.console.wraps import model_validate from extensions.ext_application_services import application_services from fields.base import ResponseModel from libs.helper import build_icon_url, dump_response -from libs.login import login_required -from models import Account +from machinery.context import RequestContext from services.recommended_app_query_service import RecommendedAppNotFoundError as RecommendedAppQueryNotFoundError class RecommendedAppsQuery(BaseModel): - language: str | None = Field(default=None, description="Language code for recommended app localization") + language: str = Field(default="en-US", description="Language code for recommended app localization") class RecommendedAppInfoResponse(ResponseModel): @@ -97,16 +97,13 @@ register_response_schema_models( class RecommendedAppListApi(Resource): @console_ns.doc(params=query_params_from_model(RecommendedAppsQuery)) @console_ns.response(200, "Success", console_ns.models[RecommendedAppListResponse.__name__]) - @login_required - @account_initialization_required - @with_current_user + @console_account_admission() @model_validate(RecommendedAppsQuery) - def get(self, req_data: RecommendedAppsQuery, current_user: Account): + def get(self, req_data: RecommendedAppsQuery, _request_context: RequestContext): return dump_response( RecommendedAppListResponse, application_services().recommended_app_queries.list_recommended( - requested_language=req_data.language, - interface_language=current_user.interface_language, + language=req_data.language, ), ) @@ -115,16 +112,13 @@ class RecommendedAppListApi(Resource): class LearnDifyAppListApi(Resource): @console_ns.doc(params=query_params_from_model(RecommendedAppsQuery)) @console_ns.response(200, "Success", console_ns.models[LearnDifyAppListResponse.__name__]) - @login_required - @account_initialization_required - @with_current_user + @console_account_admission() @model_validate(RecommendedAppsQuery) - def get(self, req_data: RecommendedAppsQuery, current_user: Account): + def get(self, req_data: RecommendedAppsQuery, _request_context: RequestContext): return dump_response( LearnDifyAppListResponse, application_services().recommended_app_queries.list_learn_dify( - requested_language=req_data.language, - interface_language=current_user.interface_language, + language=req_data.language, ), ) @@ -133,9 +127,8 @@ class LearnDifyAppListApi(Resource): class RecommendedAppApi(Resource): @console_ns.response(200, "Success", console_ns.models[RecommendedAppDetailResponse.__name__]) @console_ns.response(404, "Recommended app not found") - @login_required - @account_initialization_required - def get(self, app_id: UUID): + @console_account_admission() + def get(self, _request_context: RequestContext, app_id: UUID): try: result = application_services().recommended_app_queries.get_detail(str(app_id)) except RecommendedAppQueryNotFoundError: diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index 70ddbe73455..0fa45b01005 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -6507,7 +6507,7 @@ Check if dataset is in use | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| language | query | Language code for recommended app localization | No | string | +| language | query | Language code for recommended app localization | No | string,
**Default:** en-US | #### Responses @@ -6520,7 +6520,7 @@ Check if dataset is in use | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| language | query | Language code for recommended app localization | No | string | +| language | query | Language code for recommended app localization | No | string,
**Default:** en-US | #### Responses @@ -21331,7 +21331,7 @@ Model class for provider quota configuration. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| language | string | Language code for recommended app localization | No | +| language | string,
**Default:** en-US | Language code for recommended app localization | No | #### RedirectResponse diff --git a/api/services/explore_banner_query_service.py b/api/services/explore_banner_query_service.py index 8ef4f3827f4..4eb204f7d85 100644 --- a/api/services/explore_banner_query_service.py +++ b/api/services/explore_banner_query_service.py @@ -7,6 +7,8 @@ from collections.abc import Sequence from datetime import datetime from typing import Any, NamedTuple, Protocol +from constants.languages import languages + _DEFAULT_LANGUAGE = "en-US" @@ -37,6 +39,7 @@ class ExploreBannerQueryService: if not self._enabled: return () + language = language if language in languages else _DEFAULT_LANGUAGE banners = tuple(self._banners.list_enabled(language)) if banners or language == _DEFAULT_LANGUAGE: return banners diff --git a/api/services/recommended_app_query_service.py b/api/services/recommended_app_query_service.py index 5b79f40d505..f0da2259797 100644 --- a/api/services/recommended_app_query_service.py +++ b/api/services/recommended_app_query_service.py @@ -5,6 +5,8 @@ from typing import NamedTuple, Protocol from constants.languages import languages +_DEFAULT_LANGUAGE = "en-US" + class RecommendedAppInfoRecord(NamedTuple): id: str @@ -116,10 +118,9 @@ class RecommendedAppQueryService: def list_recommended( self, *, - requested_language: str | None, - interface_language: str | None, + language: str, ) -> RecommendedAppListResult: - language = self._resolve_language(requested_language, interface_language) + language = language if language in languages else _DEFAULT_LANGUAGE page = self._catalog.list_recommended(language) return RecommendedAppListResult( @@ -130,10 +131,9 @@ class RecommendedAppQueryService: def list_learn_dify( self, *, - requested_language: str | None, - interface_language: str | None, + language: str, ) -> LearnDifyAppListResult: - language = self._resolve_language(requested_language, interface_language) + language = language if language in languages else _DEFAULT_LANGUAGE page = self._catalog.list_learn_dify(language) return LearnDifyAppListResult(recommended_apps=self._with_trial_status(page.recommended_apps)) @@ -176,11 +176,3 @@ class RecommendedAppQueryService: ) for app in apps ) - - @staticmethod - def _resolve_language(requested_language: str | None, interface_language: str | None) -> str: - if requested_language and requested_language in languages: - return requested_language - if interface_language: - return interface_language - return languages[0] diff --git a/api/tests/unit_tests/controllers/console/explore/test_banner.py b/api/tests/unit_tests/controllers/console/explore/test_banner.py index 5c44ea4b8f5..9ade4ba82ad 100644 --- a/api/tests/unit_tests/controllers/console/explore/test_banner.py +++ b/api/tests/unit_tests/controllers/console/explore/test_banner.py @@ -110,7 +110,7 @@ class TestExploreBannerQueryService: assert service.list_for_language("fr-FR") == (record,) assert banners.requested_languages == ["fr-FR"] - def test_falls_back_to_en_us(self) -> None: + def test_falls_back_to_en_us_when_requested_translation_is_missing(self) -> None: record = _record(title="fallback") banners = FakeExploreBannerQuery({"en-US": (record,)}) service = ExploreBannerQueryService(banners=banners, enabled=True) @@ -118,6 +118,14 @@ class TestExploreBannerQueryService: assert service.list_for_language("es-ES") == (record,) assert banners.requested_languages == ["es-ES", "en-US"] + def test_invalid_language_uses_en_us(self) -> None: + record = _record(title="fallback") + banners = FakeExploreBannerQuery({"en-US": (record,)}) + service = ExploreBannerQueryService(banners=banners, enabled=True) + + assert service.list_for_language("invalid") == (record,) + assert banners.requested_languages == ["en-US"] + def test_does_not_repeat_default_language_query(self) -> None: banners = FakeExploreBannerQuery() service = ExploreBannerQueryService(banners=banners, enabled=True) diff --git a/api/tests/unit_tests/controllers/console/explore/test_recommended_app.py b/api/tests/unit_tests/controllers/console/explore/test_recommended_app.py index fccbba38d6d..e281a22caac 100644 --- a/api/tests/unit_tests/controllers/console/explore/test_recommended_app.py +++ b/api/tests/unit_tests/controllers/console/explore/test_recommended_app.py @@ -7,7 +7,7 @@ from flask import Flask from pydantic import ValidationError import controllers.console.explore.recommended_app as module -from models import Account +from machinery.context import RequestContext from models.model import AppMode, IconType from services.recommended_app_query_service import ( LearnDifyAppListResult, @@ -21,11 +21,13 @@ from services.recommended_app_query_service import ( ) -def make_account(interface_language: str | None) -> Account: - account = Account(name="Test User", email="user@example.com") - account.id = "account-1" - account.interface_language = interface_language - return account +def _request_context() -> RequestContext: + return RequestContext( + request_id="request-1", + trace_id="trace-1", + account_id="account-1", + active_workspace_id="workspace-1", + ) class TestRecommendedAppListApi: @@ -44,17 +46,16 @@ class TestRecommendedAppListApi: return_value=SimpleNamespace(recommended_app_queries=queries), ), ): - result = method(api, module.RecommendedAppsQuery(language="en-US"), make_account("fr-FR")) + result = method(api, module.RecommendedAppsQuery(language="en-US"), _request_context()) queries.list_recommended.assert_called_once_with( - requested_language="en-US", - interface_language="fr-FR", + language="en-US", ) assert result == {"recommended_apps": [], "categories": []} class TestLearnDifyAppListApi: - def test_get_with_language_param(self, app: Flask) -> None: + def test_get_uses_default_language(self, app: Flask) -> None: api = module.LearnDifyAppListApi() method = unwrap(api.get) @@ -62,18 +63,17 @@ class TestLearnDifyAppListApi: queries.list_learn_dify.return_value = LearnDifyAppListResult(recommended_apps=()) with ( - app.test_request_context("/", query_string={"language": "en-US"}), + app.test_request_context("/"), patch.object( module, "application_services", return_value=SimpleNamespace(recommended_app_queries=queries), ), ): - result = method(api, module.RecommendedAppsQuery(language="en-US"), make_account("fr-FR")) + result = method(api, module.RecommendedAppsQuery(), _request_context()) queries.list_learn_dify.assert_called_once_with( - requested_language="en-US", - interface_language="fr-FR", + language="en-US", ) assert result == {"recommended_apps": []} @@ -102,7 +102,7 @@ class TestRecommendedAppApi: return_value=SimpleNamespace(recommended_app_queries=queries), ), ): - result = method(api, "11111111-1111-1111-1111-111111111111") + result = method(api, _request_context(), "11111111-1111-1111-1111-111111111111") queries.get_detail.assert_called_once_with("11111111-1111-1111-1111-111111111111") assert result == { @@ -130,7 +130,7 @@ class TestRecommendedAppApi: ), ): with pytest.raises(module.RecommendedAppNotFoundError) as exc_info: - method(api, "11111111-1111-1111-1111-111111111111") + method(api, _request_context(), "11111111-1111-1111-1111-111111111111") assert exc_info.value.data == { "code": "recommended_app_not_found", diff --git a/api/tests/unit_tests/extensions/test_ext_application_services.py b/api/tests/unit_tests/extensions/test_ext_application_services.py index 649958ca5bf..39f8f2c10f6 100644 --- a/api/tests/unit_tests/extensions/test_ext_application_services.py +++ b/api/tests/unit_tests/extensions/test_ext_application_services.py @@ -721,14 +721,12 @@ def test_build_application_services_wires_dynamic_recommended_catalog( ) with patch.object(recommended_app_catalog_gateway.Path, "read_text", return_value=builtin_payload): result = services.recommended_app_queries.list_recommended( - requested_language="en-US", - interface_language=None, + language="en-US", ) assert result.recommended_apps apply_config_overrides(monkeypatch, HOSTED_FETCH_APP_TEMPLATES_MODE="invalid") with pytest.raises(ValueError, match="invalid fetch recommended apps mode: invalid"): services.recommended_app_queries.list_recommended( - requested_language="en-US", - interface_language=None, + language="en-US", ) diff --git a/api/tests/unit_tests/services/test_recommended_app_query_service.py b/api/tests/unit_tests/services/test_recommended_app_query_service.py index 09d661b6754..3e762b12167 100644 --- a/api/tests/unit_tests/services/test_recommended_app_query_service.py +++ b/api/tests/unit_tests/services/test_recommended_app_query_service.py @@ -2,7 +2,6 @@ from unittest.mock import MagicMock import pytest -from constants.languages import languages from services.recommended_app_query_service import ( RecommendedAppCatalogPage, RecommendedAppDetailRecord, @@ -83,17 +82,14 @@ def test_is_previewable_falls_back_to_catalog(expected: bool) -> None: @pytest.mark.parametrize( - ("requested_language", "interface_language", "expected"), + ("language", "expected"), [ - ("en-US", "fr-FR", "en-US"), - ("invalid", "fr-FR", "fr-FR"), - (None, "custom-language", "custom-language"), - (None, None, languages[0]), + ("fr-FR", "fr-FR"), + ("invalid", "en-US"), ], ) def test_list_recommended_resolves_language( - requested_language: str | None, - interface_language: str | None, + language: str, expected: str, ) -> None: catalog = MagicMock() @@ -101,8 +97,7 @@ def test_list_recommended_resolves_language( service, _ = _service(catalog=catalog) service.list_recommended( - requested_language=requested_language, - interface_language=interface_language, + language=language, ) catalog.list_recommended.assert_called_once_with(expected) @@ -113,7 +108,7 @@ def test_list_recommended_disables_upstream_trial_without_querying_trial_apps() catalog.list_recommended.return_value = _page("app-1") service, trial_apps = _service(catalog=catalog) - result = service.list_recommended(requested_language="en-US", interface_language=None) + result = service.list_recommended(language="en-US") assert result.recommended_apps[0].can_trial is False trial_apps.existing_ids.assert_not_called() @@ -130,7 +125,7 @@ def test_list_recommended_enriches_trial_status_in_one_bulk_query() -> None: trial_enabled=True, ) - result = service.list_recommended(requested_language="en-US", interface_language=None) + result = service.list_recommended(language="en-US") assert [app.can_trial for app in result.recommended_apps] == [True, False] trial_apps.existing_ids.assert_called_once_with(["app-1", "app-2"]) @@ -141,9 +136,9 @@ def test_list_learn_dify_does_not_return_categories() -> None: catalog.list_learn_dify.return_value = _page(categories=("ignored",)) service, _ = _service(catalog=catalog) - result = service.list_learn_dify(requested_language="invalid", interface_language="fr-FR") + result = service.list_learn_dify(language="invalid") - catalog.list_learn_dify.assert_called_once_with("fr-FR") + catalog.list_learn_dify.assert_called_once_with("en-US") assert result.recommended_apps == () assert not hasattr(result, "categories") diff --git a/packages/contracts/generated/api/console/explore/zod.gen.ts b/packages/contracts/generated/api/console/explore/zod.gen.ts index 272b2884146..995a5b98a58 100644 --- a/packages/contracts/generated/api/console/explore/zod.gen.ts +++ b/packages/contracts/generated/api/console/explore/zod.gen.ts @@ -137,7 +137,7 @@ export const zLearnDifyAppListResponseWritable = z.object({ }) export const zGetExploreAppsQuery = z.object({ - language: z.string().optional(), + language: z.string().optional().default('en-US'), }) /** @@ -146,7 +146,7 @@ export const zGetExploreAppsQuery = z.object({ export const zGetExploreAppsResponse = zRecommendedAppListResponse export const zGetExploreAppsLearnDifyQuery = z.object({ - language: z.string().optional(), + language: z.string().optional().default('en-US'), }) /**