diff --git a/geonode/documents/api/tests.py b/geonode/documents/api/tests.py index 542d5f4c74f..42f677a189e 100644 --- a/geonode/documents/api/tests.py +++ b/geonode/documents/api/tests.py @@ -31,6 +31,7 @@ from geonode.base.populate_test_data import create_models from geonode.base.enumerations import SOURCE_TYPE_REMOTE from geonode.documents.models import Document +from geonode.metadata.models import SparseField from geonode.security.registry import permissions_registry logger = logging.getLogger(__name__) @@ -622,3 +623,32 @@ def test_either_path_or_url_doc(self): actual = self.client.post(self.url, data=payload, format="json") self.assertEqual(400, actual.status_code) self.assertDictEqual(expected, actual.json()) + + def test_documents_api_include_i18n(self): + """ + Ensure that the ?include_i18n parameter returns all localized fields in the Documents API response. + """ + + _document = Document.objects.first() + # Manually create sparse fields for the document to test prefetching/serialization + SparseField.objects.create(resource=_document, name="title_multilang_en", value="English Doc Title") + SparseField.objects.create(resource=_document, name="title_multilang_it", value="Titolo Documento Italiano") + + url = reverse("documents-detail", kwargs={"pk": _document.pk}) + + with override_settings( + LANGUAGES=[("en", "English"), ("it", "Italiano")], + MULTILANG_FIELDS=["title"], + ): + query_params = "include_i18n=true&lang=it" + response = self.client.get(f"{url}?{query_params}", format="json") + + self.assertEqual(response.status_code, 200) + data = response.data["document"] + + self.assertEqual(data["title"], "Titolo Documento Italiano") + + self.assertIn("title_en", data) + self.assertEqual(data["title_en"], "English Doc Title") + self.assertIn("title_it", data) + self.assertEqual(data["title_it"], "Titolo Documento Italiano") diff --git a/geonode/layers/api/tests.py b/geonode/layers/api/tests.py index 4e6c69d11d2..a68e61c68ec 100644 --- a/geonode/layers/api/tests.py +++ b/geonode/layers/api/tests.py @@ -33,6 +33,7 @@ from geonode.base.populate_test_data import create_models, create_single_dataset from geonode.layers.models import Attribute, Dataset from geonode.maps.models import Map, MapLayer +from geonode.metadata.models import SparseField logger = logging.getLogger(__name__) @@ -692,3 +693,33 @@ def test_download_api(self): self.assertEqual(download_url_data["default"], True) self.assertEqual(download_url_data["ajax_safe"], False) self.assertEqual(download_url_data["url"], "https://myoriginal.org") + + def test_datasets_api_include_i18n(self): + """ + Ensure that the ?include_i18n parameter returns all localized fields in the API response. + """ + + _dataset = Dataset.objects.first() + + # Manually create the sparse fields required for the test + SparseField.objects.create(resource=_dataset, name="title_multilang_en", value="English Title") + SparseField.objects.create(resource=_dataset, name="title_multilang_it", value="Titolo Italiano") + + url = reverse("datasets-detail", kwargs={"pk": _dataset.pk}) + + with override_settings( + LANGUAGES=[("en", "English"), ("it", "Italiano")], + MULTILANG_FIELDS=["title"], + ): + query_params = "include_i18n=true&lang=it" + response = self.client.get(f"{url}?{query_params}", format="json") + + self.assertEqual(response.status_code, 200) + data = response.data["dataset"] + + self.assertEqual(data["title"], "Titolo Italiano") + + self.assertIn("title_en", data) + self.assertEqual(data["title_en"], "English Title") + self.assertIn("title_it", data) + self.assertEqual(data["title_it"], "Titolo Italiano") diff --git a/geonode/metadata/multilang/serializers.py b/geonode/metadata/multilang/serializers.py index d8bf86ede2d..4c8b2fef1d5 100644 --- a/geonode/metadata/multilang/serializers.py +++ b/geonode/metadata/multilang/serializers.py @@ -3,6 +3,8 @@ from django.conf import settings from rest_framework import serializers +from geonode.metadata.multilang import utils as multi + logger = logging.getLogger(__name__) @@ -12,12 +14,27 @@ class MultiLangOutputMixin(serializers.BaseSerializer): def to_representation(self, instance): representation = super().to_representation(instance) + request = self.context.get("request") + params = getattr(request, "query_params", None) or getattr(request, "GET", {}) + include_i18n = params.get("include_i18n", "false").lower() == "true" if request else False + target_lang = multi.get_language(request) if settings.MULTILANG_FIELDS and hasattr(instance, "_multilang_sparse_prefetch"): - for sparse in instance._multilang_sparse_prefetch: - base_field_name = sparse.name[:-13] # name_multilang_xx - if base_field_name in representation: - logger.debug(f"setting into {instance} field {base_field_name} --> {sparse.value}") - representation[base_field_name] = sparse.value + multilang_field_map = multi.get_all_multilang_fields() + + sparse_value_map = {sparse.name: sparse.value for sparse in instance._multilang_sparse_prefetch} + + for (base_field_name, lang_code), sparse_field_name in multilang_field_map.items(): + + if sparse_field_name not in sparse_value_map: + continue + + value = sparse_value_map[sparse_field_name] + + if lang_code == target_lang: + representation[base_field_name] = value + + if include_i18n: + representation[f"{base_field_name}_{lang_code}"] = value return representation diff --git a/geonode/metadata/multilang/utils.py b/geonode/metadata/multilang/utils.py index 83c62c4aec2..d92b05d7490 100644 --- a/geonode/metadata/multilang/utils.py +++ b/geonode/metadata/multilang/utils.py @@ -36,11 +36,14 @@ def get_default_language(): def get_language(request): if request: - language = request.query_params.get("lang", None) # explicit query param + params = getattr(request, "query_params", None) or getattr(request, "GET", {}) + language = params.get("lang", None) + if not language: language = getattr(request, "LANGUAGE_CODE", None) # LocaleMiddleware if not language: - language = request.headers.get("Accept-Language", "").split(",")[0] + headers = getattr(request, "headers", {}) + language = headers.get("Accept-Language", "").split(",")[0] else: language = get_default_language() @@ -48,3 +51,11 @@ def get_language(request): language = language.split("-")[0] # normalize return language + + +def get_all_multilang_fields(): + return { + (field, lang): get_multilang_field_name(field, lang) + for field in settings.MULTILANG_FIELDS + for lang in get_2letters_languages() + } diff --git a/geonode/metadata/multilang/views.py b/geonode/metadata/multilang/views.py index dc92ed70530..dc1e8ff49b5 100644 --- a/geonode/metadata/multilang/views.py +++ b/geonode/metadata/multilang/views.py @@ -17,15 +17,24 @@ class MultiLangViewMixin: def get_queryset(self): """Adds a Prefetch to include localized fields in one query.""" - lang = multi.get_language(getattr(self, "request", None)) - field_names = multi.get_multilang_fields_for_lang(lang) + + request = getattr(self, "request", None) + params = getattr(request, "query_params", None) or getattr(request, "GET", {}) + include_i18n = params.get("include_i18n", "false").lower() == "true" if request else False + lang = multi.get_language(request) + + if include_i18n: + field_map = multi.get_all_multilang_fields() + field_names = list(field_map.values()) + else: + field_names = multi.get_multilang_fields_for_lang(lang) qs = super().get_queryset() if not field_names: return qs - # Prefetch all localized rows for this language + # Prefetch the localized rows prefetch = Prefetch( "sparsefield_set", # this must match related_name on FK queryset=SparseField.objects.filter(name__in=field_names), diff --git a/geonode/metadata/tests/test_multilang.py b/geonode/metadata/tests/test_multilang.py index 10f887bd4dc..29c5cda1d3f 100644 --- a/geonode/metadata/tests/test_multilang.py +++ b/geonode/metadata/tests/test_multilang.py @@ -136,6 +136,23 @@ def test_default_preserialization(self): self.assertEqual("title_fake", instance["title_multilang_it"]) self.assertIsNone(instance["title_multilang_en"]) + def test_get_all_multilang_fields_structure(self): + """ + Verify the new dictionary structure and ensure it identifies + all configured field/language combinations. + """ + with override_settings( + LANGUAGES=[("en", "English"), ("it", "Italiano")], + MULTILANG_FIELDS=["title", "abstract"], + ): + field_map = multi.get_all_multilang_fields() + + self.assertIsInstance(field_map, dict) + self.assertEqual(len(field_map), 4) + + self.assertEqual(field_map[("title", "en")], "title_multilang_en") + self.assertEqual(field_map[("abstract", "it")], "abstract_multilang_it") + @patch("geonode.base.models.ResourceBase.get_real_instance_class") @patch("geonode.indexing.manager.TSVectorIndexManager.update_index") @patch("geonode.metadata.handlers.sparse.SparseHandler.update_resource")