mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 00:31:19 +08:00
refactor: make explore localization request-driven (#41798)
This commit is contained in:
parent
8504aaa0ef
commit
cea89f784f
@ -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:
|
||||
|
||||
@ -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, <br>**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, <br>**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, <br>**Default:** en-US | Language code for recommended app localization | No |
|
||||
|
||||
#### RedirectResponse
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
)
|
||||
|
||||
@ -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")
|
||||
|
||||
|
||||
@ -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'),
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user