Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions src/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from rest_framework import serializers, validators

from django.conf import settings
from django.db import transaction
from django.shortcuts import reverse
from modeltranslation.utils import build_localized_fieldname

from core import models as core_models, logic as core_logic
from journal import models as journal_models
Expand All @@ -13,6 +15,55 @@
from events import logic as event_logic


def translatable_repository(instance):
"""Return the repository governing the active languages for a translatable
preprint object (Preprint, PreprintVersion or VersionQueue)."""
if hasattr(instance, "repository"):
return instance.repository
return instance.preprint.repository


def apply_translations(instance, field_name, translations):
"""Set per-language values for a modeltranslation-managed field from a
``{language_code: value}`` mapping. Does not save the instance."""
if not translations:
return
for code, value in translations.items():
setattr(instance, build_localized_fieldname(field_name, code), value)


class TranslationsField(serializers.DictField):
"""A readable and writable ``{language_code: value}`` map for a
modeltranslation-managed field. On read it exposes one entry per language
the preprint's repository accepts; on write it accepts a partial mapping."""

def __init__(self, translated_field, **kwargs):
self.translated_field = translated_field
kwargs.setdefault("required", False)
kwargs.setdefault(
"child",
serializers.CharField(allow_blank=True, trim_whitespace=False),
)
super().__init__(**kwargs)

def get_attribute(self, instance):
# to_representation builds the map from the instance itself.
return instance

def to_representation(self, instance):
repository = translatable_repository(instance)
languages = repository.languages or [settings.LANGUAGE_CODE]
return {
code: getattr(
instance,
build_localized_fieldname(self.translated_field, code),
"",
)
or ""
for code in languages
}


class LicenceSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = submission_models.Licence
Expand Down Expand Up @@ -202,9 +253,14 @@ class Meta:
"date_time",
"title",
"abstract",
"title_translations",
"abstract_translations",
"public_download_url",
)

title_translations = TranslationsField("title", read_only=True)
abstract_translations = TranslationsField("abstract", read_only=True)


class PreprintSupplementaryFileSerializer(serializers.ModelSerializer):
class Meta:
Expand Down Expand Up @@ -372,6 +428,8 @@ class Meta:
"pk",
"title",
"abstract",
"title_translations",
"abstract_translations",
"stage",
"license",
"keywords",
Expand All @@ -389,6 +447,8 @@ class Meta:
)
depth = 2

title_translations = TranslationsField("title", read_only=True)
abstract_translations = TranslationsField("abstract", read_only=True)
authors = PreprintAccountSerializer(
many=True,
)
Expand Down Expand Up @@ -436,6 +496,15 @@ def create(self, validated_data):
comments_editor=validated_data.get("comments_editor"),
)

apply_translations(preprint, "title", validated_data.get("title_translations"))
apply_translations(
preprint, "abstract", validated_data.get("abstract_translations")
)
if validated_data.get("title_translations") or validated_data.get(
"abstract_translations"
):
preprint.save()

for i, author_data in enumerate(validated_data.get("authors", [])):
author_email = author_data.pop("email").lower()
author, created = core_models.Account.objects.get_or_create(
Expand Down Expand Up @@ -530,6 +599,10 @@ def update(self, instance, validated_data):
instance.doi = validated_data.get("doi")
instance.preprint_doi = validated_data.get("preprint_doi")
instance.comments_editor = validated_data.get("comments_editor")
apply_translations(instance, "title", validated_data.get("title_translations"))
apply_translations(
instance, "abstract", validated_data.get("abstract_translations")
)
instance.save()

authors = []
Expand Down Expand Up @@ -633,6 +706,8 @@ class Meta:
"authors",
"title",
"abstract",
"title_translations",
"abstract_translations",
"stage",
"license",
"keywords",
Expand All @@ -649,6 +724,8 @@ class Meta:
"comments_editor",
)

title_translations = TranslationsField("title")
abstract_translations = TranslationsField("abstract")
authors = PreprintAccountSerializer(
many=True,
)
Expand Down Expand Up @@ -689,10 +766,15 @@ class Meta:
"update_type",
"title",
"abstract",
"title_translations",
"abstract_translations",
"published_doi",
"file",
)

title_translations = TranslationsField("title")
abstract_translations = TranslationsField("abstract")

def validate(self, data):
request = self.context.get("request", None)
preprint = data.get("preprint")
Expand All @@ -707,6 +789,16 @@ def validate(self, data):

return data

def create(self, validated_data):
title_translations = validated_data.pop("title_translations", None)
abstract_translations = validated_data.pop("abstract_translations", None)
version_queue = super().create(validated_data)
apply_translations(version_queue, "title", title_translations)
apply_translations(version_queue, "abstract", abstract_translations)
if title_translations or abstract_translations:
version_queue.save()
return version_queue


class VersionQueueSerializer(serializers.ModelSerializer):
class Meta:
Expand All @@ -720,9 +812,14 @@ class Meta:
"published_doi",
"title",
"abstract",
"title_translations",
"abstract_translations",
"file",
)

title_translations = TranslationsField("title", read_only=True)
abstract_translations = TranslationsField("abstract", read_only=True)


class RegisterAccountSerializer(serializers.ModelSerializer):
class Meta:
Expand Down
120 changes: 120 additions & 0 deletions src/api/tests/test_preprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from rest_framework.test import APIClient

from api import serializers as api_serializers
from identifiers import forms as identifier_forms
from identifiers import models as identifier_models
from repository import models as repository_models
Expand Down Expand Up @@ -522,3 +523,122 @@ def test_owner_sees_only_own_files(self):
}
self.assertNotIn(self.preprint_other.pk, preprint_ids)
self.api_client.force_authenticate(user=None)


MULTILINGUAL_API_DOMAIN = "preprint-api-multilingual.domain.com"
MONOLINGUAL_API_DOMAIN = "preprint-api-monolingual.domain.com"


class TestPreprintMultilingualAPI(TestCase):
"""
Regression tests for multilingual title/abstract support in the preprint
API serializers (#3375). The repository uses django-modeltranslation, so
the serializers expose and accept a {language_code: value} map alongside
the primary title/abstract.
"""

@classmethod
def setUpTestData(cls):
cls.press = helpers.create_press()
cls.author = helpers.create_user("preprint.api.multilingual@test.com")

cls.repo, cls.subject = helpers.create_repository(
cls.press, [], [], domain=MULTILINGUAL_API_DOMAIN
)
cls.repo.languages = ["en", "es"]
cls.repo.default_language = "en"
cls.repo.save()

cls.mono_repo, cls.mono_subject = helpers.create_repository(
cls.press, [], [], domain=MONOLINGUAL_API_DOMAIN
)
cls.mono_repo.languages = ["en"]
cls.mono_repo.save()

cls.preprint = helpers.create_preprint(
cls.repo, cls.author, cls.subject, title="English Title"
)
cls.preprint.title_en = "English Title"
cls.preprint.title_es = "Título en español"
cls.preprint.abstract_en = "English abstract"
cls.preprint.abstract_es = "Resumen en español"
cls.preprint.save()

cls.mono_preprint = helpers.create_preprint(
cls.mono_repo, cls.author, cls.mono_subject, title="Mono Title"
)

def test_read_serializer_exposes_translations_for_active_languages(self):
"""The read serializer returns one entry per repository language."""
data = api_serializers.PreprintSerializer(self.preprint).data
self.assertEqual(
data["title_translations"],
{"en": "English Title", "es": "Título en español"},
)
self.assertEqual(
data["abstract_translations"],
{"en": "English abstract", "es": "Resumen en español"},
)

def test_read_serializer_single_language_repository(self):
"""A single-language repository exposes a single translation entry."""
data = api_serializers.PreprintSerializer(self.mono_preprint).data
self.assertEqual(
data["title_translations"],
{"en": "Mono Title"},
)

def test_update_serializer_applies_translations(self):
"""Updating via the create/update serializer writes per-language fields."""
validated_data = {
"title": "Updated English",
"abstract": "Updated English abstract",
"title_translations": {
"en": "Updated English",
"es": "Actualizado en español",
},
"abstract_translations": {"es": "Resumen actualizado"},
"owner": self.author,
"repository": self.repo,
"stage": repository_models.STAGE_PREPRINT_REVIEW,
"license": None,
"date_submitted": self.preprint.date_submitted,
"date_accepted": None,
"date_published": None,
"doi": None,
"preprint_doi": None,
"comments_editor": "",
"authors": [],
"keywords": [],
"subject": [],
"repositoryfieldanswer_set": [],
"preprintsupplementaryfile_set": [],
}
serializer = api_serializers.PreprintCreateSerializer()
result = serializer.update(self.preprint, validated_data)
result.refresh_from_db()

self.assertEqual(result.title_en, "Updated English")
self.assertEqual(result.title_es, "Actualizado en español")
self.assertEqual(result.abstract_es, "Resumen actualizado")

def test_version_queue_create_applies_translations(self):
"""Creating a version queue entry writes per-language fields."""
serializer = api_serializers.VersionQueueCreateSerializer()
version_queue = serializer.create(
{
"preprint": self.preprint,
"update_type": "correction",
"title": "Version English",
"abstract": "Version English abstract",
"title_translations": {
"en": "Version English",
"es": "Versión en español",
},
"abstract_translations": {"es": "Resumen de la versión"},
}
)
version_queue.refresh_from_db()

self.assertEqual(version_queue.title_es, "Versión en español")
self.assertEqual(version_queue.abstract_es, "Resumen de la versión")
17 changes: 17 additions & 0 deletions src/journal/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,20 @@ def process_request(request):
request.available_languages = set(available_languages)
request.default_language = default_language
request.current_language = translation.get_language()

elif getattr(request, "repository", None) and settings.USE_I18N:
current_language = translation.get_language()
available_languages = list(request.repository.languages or [])
default_language = (
request.repository.default_language or settings.LANGUAGE_CODE
)

if current_language not in available_languages:
translation.activate(default_language)

if not available_languages:
available_languages = [lang[0] for lang in settings.LANGUAGES]

request.available_languages = set(available_languages)
request.default_language = default_language
request.current_language = translation.get_language()
Loading
Loading