diff --git a/apps/accounts/serializers.py b/apps/accounts/serializers.py index 1b56840d..dc00a872 100644 --- a/apps/accounts/serializers.py +++ b/apps/accounts/serializers.py @@ -29,7 +29,7 @@ from apps.skills.models import Skill from apps.skills.serializers import SkillLightSerializer, TagRelatedField from services.crisalid.serializers import ResearcherSerializerLight -from services.translator.serializers import AutoTranslatedModelSerializer +from services.translator.serializers import auto_translated from .exceptions import ( FeaturedProjectPermissionDeniedError, @@ -65,9 +65,8 @@ class Meta: ] -class UserAdminListSerializer( - AutoTranslatedModelSerializer, serializers.ModelSerializer -): +@auto_translated +class UserAdminListSerializer(serializers.ModelSerializer): current_org_role = serializers.CharField(required=False, read_only=True) email_verified = serializers.BooleanField(required=False, read_only=True) people_groups = serializers.SerializerMethodField() @@ -104,7 +103,8 @@ def get_people_groups(self, user: ProjectUser) -> list: ).data -class UserLighterSerializer(AutoTranslatedModelSerializer, serializers.ModelSerializer): +@auto_translated +class UserLighterSerializer(serializers.ModelSerializer): email = PrivacySettingProtectedEmailField( privacy_field="email", required=False, allow_blank=True ) @@ -149,7 +149,8 @@ def to_representation(self, instance: ProjectUser) -> dict[str, Any]: } -class UserLightSerializer(AutoTranslatedModelSerializer, serializers.ModelSerializer): +@auto_translated +class UserLightSerializer(serializers.ModelSerializer): email = PrivacySettingProtectedEmailField( privacy_field="email", required=False, allow_blank=True ) @@ -236,9 +237,8 @@ def get_can_mentor_on(self, user: ProjectUser) -> list[dict]: return [] -class PeopleGroupSuperLightSerializer( - AutoTranslatedModelSerializer, serializers.ModelSerializer -): +@auto_translated +class PeopleGroupSuperLightSerializer(serializers.ModelSerializer): organization = serializers.SlugRelatedField(read_only=True, slug_field="code") class Meta: @@ -253,6 +253,7 @@ class Meta: fields = read_only_fields +@auto_translated class PeopleGroupLocationSerializer(BaseLocationSerializer): people_group = serializers.PrimaryKeyRelatedField( required=False, queryset=PeopleGroup.objects.all() @@ -284,9 +285,9 @@ def create(self, validated_data): return PeopleGroupLocation.objects.bulk_create(locations_objs) +@auto_translated class PeopleGroupLightSerializer( ModulesSerializers, - AutoTranslatedModelSerializer, serializers.ModelSerializer, ): header_image = ImageSerializer(read_only=True) @@ -312,9 +313,9 @@ class Meta: modules_keys = ("members", "subgroups") +@auto_translated class PeopleGroupHierarchySerializer( ModulesSerializers, - AutoTranslatedModelSerializer, serializers.ModelSerializer, ): children = serializers.SerializerMethodField() @@ -374,8 +375,10 @@ def get_children(self, people_group: PeopleGroup) -> list[dict[str, str | int]]: return [] if not mapping: - base_queryset = request.user.get_people_group_queryset().filter( - organization=people_group.organization + base_queryset = ( + request.user.get_people_group_queryset() + .filter(organization=people_group.organization) + .select_related("header_image") ) mapping = {group.id: group for group in base_queryset} context["mapping"] = mapping @@ -493,10 +496,10 @@ def create(self, validated_data): return validated_data +@auto_translated class PeopleGroupSerializer( ModulesSerializers, StringsImagesSerializer, - AutoTranslatedModelSerializer, serializers.ModelSerializer, ): string_images_forbid_fields: list[str] = [ @@ -645,9 +648,9 @@ class Meta: @extend_schema_serializer(exclude_fields=("roles",)) +@auto_translated class UserSerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, serializers.ModelSerializer, ): string_images_forbid_fields: list[str] = [ diff --git a/apps/analytics/serializers.py b/apps/analytics/serializers.py index 113aa61e..38f9308c 100644 --- a/apps/analytics/serializers.py +++ b/apps/analytics/serializers.py @@ -1,11 +1,11 @@ from rest_framework import serializers -from apps.commons.serializers import TranslatedModelSerializer from apps.files.serializers import ImageSerializer from apps.organizations.models import Organization from apps.projects.models import Project from apps.skills.models import Tag from apps.skills.serializers import TagSerializer +from services.translator.serializers import external_auto_translated class StatsOrganizationSerializer(serializers.ModelSerializer): @@ -41,7 +41,8 @@ class ProjectByMonth(serializers.Serializer): updated_count = serializers.IntegerField() -class TagProjectSerializer(TranslatedModelSerializer): +@external_auto_translated +class TagProjectSerializer(serializers.ModelSerializer): projects = serializers.PrimaryKeyRelatedField( many=True, queryset=Project.objects.all() ) diff --git a/apps/announcements/serializers.py b/apps/announcements/serializers.py index d50d60f7..d585e6be 100644 --- a/apps/announcements/serializers.py +++ b/apps/announcements/serializers.py @@ -9,14 +9,13 @@ from apps.files.serializers import ImageSerializer from apps.organizations.models import Organization from apps.projects.models import Project -from services.translator.serializers import AutoTranslatedModelSerializer +from services.translator.serializers import auto_translated from .models import Announcement -class ProjectAnnouncementSerializer( - AutoTranslatedModelSerializer, serializers.ModelSerializer -): +@auto_translated +class ProjectAnnouncementSerializer(serializers.ModelSerializer): header_image = ImageSerializer(read_only=True) class Meta: @@ -32,9 +31,9 @@ class Meta: ] +@auto_translated class AnnouncementSerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, OrganizationRelatedSerializer, ProjectRelatedSerializer, serializers.ModelSerializer, @@ -82,6 +81,7 @@ def get_related_project(self) -> Project | None: return None +@auto_translated class ApplyToAnnouncementSerializer(AnnouncementSerializer): applicant_name = serializers.CharField() applicant_firstname = serializers.CharField() diff --git a/apps/commons/serializers.py b/apps/commons/serializers.py index a4f17d78..7d4a42ac 100644 --- a/apps/commons/serializers.py +++ b/apps/commons/serializers.py @@ -1,10 +1,8 @@ from collections.abc import Collection from typing import Any -from django.conf import settings from django.db.models import Model, Q from django.utils.translation import gettext_lazy as _ -from modeltranslation.manager import get_translatable_fields_for_model from rest_framework import mixins, serializers, viewsets from rest_framework.settings import import_from_string @@ -13,7 +11,6 @@ from apps.files.models import Image from apps.organizations.models import Organization from apps.projects.models import Project -from services.translator.serializers import AutoTranslatedModelSerializer class ProjectRelatedSerializer(serializers.ModelSerializer): @@ -85,33 +82,6 @@ def __getattribute__(self, attr): return object.__getattribute__(self, attr) -class TranslatedModelSerializer(serializers.ModelSerializer): - """ - Automatically translate model fields for model with registered translation - """ - - def get_field_names(self, declared_fields, info): - fields = super().get_field_names(declared_fields, info) - translated_fields = get_translatable_fields_for_model(self.Meta.model) or [] - all_fields = [] - - requested_langs = [] - if "request" in self.context: - lang_param = self.context["request"].query_params.get("lang") - requested_langs = lang_param.split(",") if lang_param else [] - - for field in fields: - all_fields.append(field) - if field in translated_fields: - for lang in settings.REQUIRED_LANGUAGES: - if not requested_langs or lang in requested_langs: - all_fields.append(f"{field}_{lang}") - return all_fields - - class Meta: - model = None - - class RetrieveUpdateModelViewSet( mixins.RetrieveModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet ): @@ -226,7 +196,6 @@ def save(self, **kwargs): class BaseLocationSerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, serializers.ModelSerializer, ): string_images_forbid_fields: list[str] = ["title", "description"] diff --git a/apps/feedbacks/serializers.py b/apps/feedbacks/serializers.py index c6db6538..afe16f60 100644 --- a/apps/feedbacks/serializers.py +++ b/apps/feedbacks/serializers.py @@ -13,7 +13,7 @@ from apps.files.models import Image from apps.organizations.models import Organization from apps.projects.models import Project -from services.translator.serializers import AutoTranslatedModelSerializer +from services.translator.serializers import auto_translated from .exceptions import ( CommentProjectPermissionDeniedError, @@ -24,7 +24,6 @@ class FollowSerializer( - AutoTranslatedModelSerializer, OrganizationRelatedSerializer, ProjectRelatedSerializer, serializers.ModelSerializer, @@ -75,9 +74,9 @@ def create(self, validated_data): pass +@auto_translated class ReviewSerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, OrganizationRelatedSerializer, ProjectRelatedSerializer, serializers.ModelSerializer, @@ -115,9 +114,9 @@ def get_related_project(self) -> Project | None: return None +@auto_translated class CommentSerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, OrganizationRelatedSerializer, ProjectRelatedSerializer, serializers.ModelSerializer, diff --git a/apps/files/serializers.py b/apps/files/serializers.py index e26e45ec..a7076d4c 100644 --- a/apps/files/serializers.py +++ b/apps/files/serializers.py @@ -18,7 +18,7 @@ ) from apps.organizations.models import Organization from apps.projects.models import Project -from services.translator.serializers import AutoTranslatedModelSerializer +from services.translator.serializers import auto_translated from .exceptions import ( ChangeFileProjectError, @@ -172,10 +172,10 @@ def find_attachment_type(soup): return AttachmentType.LINK +@auto_translated class AttachmentLinkSerializer( StringsImagesSerializer, AbstractAttachmentLink, - AutoTranslatedModelSerializer, OrganizationRelatedSerializer, ProjectRelatedSerializer, serializers.ModelSerializer, @@ -246,9 +246,9 @@ def get_related_organizations(self) -> list[Organization]: return [] +@auto_translated class OrganizationAttachmentFileSerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, serializers.ModelSerializer, ): string_images_forbid_fields: list[str] = ["description", "title"] @@ -296,9 +296,9 @@ def validate_file(self, file): return file +@auto_translated class AttachmentFileSerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, OrganizationRelatedSerializer, ProjectRelatedSerializer, serializers.ModelSerializer, @@ -442,10 +442,10 @@ def to_representation(self, instance): } +@auto_translated class ProjectUserAttachmentLinkSerializer( AbstractAttachmentLink, StringsImagesSerializer, - AutoTranslatedModelSerializer, ): string_images_forbid_fields: list[str] = ["title", "description"] @@ -461,9 +461,8 @@ class Meta: ] -class ProjectUserAttachmentFileSerializer( - StringsImagesSerializer, AutoTranslatedModelSerializer -): +@auto_translated +class ProjectUserAttachmentFileSerializer(StringsImagesSerializer): file = serializers.FileField() string_images_forbid_fields: list[str] = ["title", "description"] diff --git a/apps/invitations/serializers.py b/apps/invitations/serializers.py index c38bf0b7..3d6a338a 100644 --- a/apps/invitations/serializers.py +++ b/apps/invitations/serializers.py @@ -16,7 +16,7 @@ ) from apps.invitations.models import AccessRequest from apps.organizations.models import Organization -from services.translator.serializers import AutoTranslatedModelSerializer +from services.translator.serializers import auto_translated from .exceptions import ( AccessRequestDisabledError, @@ -30,9 +30,9 @@ from .models import Invitation +@auto_translated class InvitationSerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, OrganizationRelatedSerializer, serializers.ModelSerializer, ): @@ -82,9 +82,9 @@ def get_related_organizations(self): return Organization.objects.filter(code=self.context.get("organization_code")) +@auto_translated class AccessRequestSerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, serializers.ModelSerializer, ): string_images_forbid_fields: list[str] = ["message"] diff --git a/apps/modules/serializers.py b/apps/modules/serializers.py index ca880925..33607207 100644 --- a/apps/modules/serializers.py +++ b/apps/modules/serializers.py @@ -10,10 +10,10 @@ class ModulesSerializers(serializers.ModelSerializer): def __init__(self, *ar, **kw): super().__init__(*ar, **kw) - request = self.context.get("request") - query = request.query_params if request else QueryDict() - if "modules_keys" not in self.context: + request = self.context.get("request") + query = request.query_params if request else QueryDict() + modules_keys = None # if modules is set queryparams , return list elements (for multiples modules) if "modules" in query: diff --git a/apps/newsfeed/serializers.py b/apps/newsfeed/serializers.py index 118828cf..852a7020 100644 --- a/apps/newsfeed/serializers.py +++ b/apps/newsfeed/serializers.py @@ -13,7 +13,7 @@ from apps.files.serializers import ImageSerializer from apps.organizations.models import Organization from apps.projects.serializers import ProjectLightSerializer -from services.translator.serializers import AutoTranslatedModelSerializer +from services.translator.serializers import auto_translated from .exceptions import ( EventPeopleGroupOrganizationError, @@ -23,9 +23,9 @@ from .models import Event, Instruction, News, Newsfeed +@auto_translated class NewsSerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, OrganizationRelatedSerializer, serializers.ModelSerializer, ): @@ -84,9 +84,9 @@ def get_string_images_kwargs( } +@auto_translated class InstructionSerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, OrganizationRelatedSerializer, serializers.ModelSerializer, ): @@ -165,9 +165,9 @@ class Meta: ] +@auto_translated class EventSerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, OrganizationRelatedSerializer, serializers.ModelSerializer, ): diff --git a/apps/notifications/serializers.py b/apps/notifications/serializers.py index d54c6849..3659fbe6 100644 --- a/apps/notifications/serializers.py +++ b/apps/notifications/serializers.py @@ -1,10 +1,10 @@ from rest_framework import serializers from apps.accounts.serializers import UserLighterSerializer -from apps.commons.serializers import TranslatedModelSerializer from apps.invitations.serializers import InvitationSerializer from apps.organizations.models import Organization from apps.projects.serializers import ProjectSuperLightSerializer +from services.translator.serializers import external_auto_translated from .models import Notification, NotificationSettings @@ -22,7 +22,8 @@ class ContactSerializer(serializers.Serializer): email = serializers.EmailField(required=True) -class NotificationsSerializer(TranslatedModelSerializer): +@external_auto_translated +class NotificationsSerializer(serializers.ModelSerializer): sender = UserLighterSerializer(read_only=True) project = ProjectSuperLightSerializer(read_only=True) invitation = InvitationSerializer(read_only=True) diff --git a/apps/organizations/serializers.py b/apps/organizations/serializers.py index 4b5118d7..451c8ba6 100644 --- a/apps/organizations/serializers.py +++ b/apps/organizations/serializers.py @@ -28,7 +28,7 @@ TagRelatedField, ) from services.keycloak.serializers import IdentityProviderSerializer -from services.translator.serializers import AutoTranslatedModelSerializer +from services.translator.serializers import auto_translated from .exceptions import ( CategoryHierarchyLoopError, @@ -50,9 +50,9 @@ logger = logging.getLogger(__name__) +@auto_translated class TermsAndConditionsSerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, serializers.ModelSerializer, ): """ @@ -203,9 +203,9 @@ def create(self, validated_data): return validated_data +@auto_translated class OrganizationSerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, OrganizationRelatedSerializer, serializers.ModelSerializer, ): @@ -378,8 +378,8 @@ def update(self, instance, validated_data): return super(OrganizationSerializer, self).update(instance, validated_data) +@auto_translated class OrganizationLightSerializer( - AutoTranslatedModelSerializer, OrganizationRelatedSerializer, serializers.ModelSerializer, ): @@ -415,8 +415,8 @@ def create(self, validated_data): return organization +@auto_translated class TemplateLightSerializer( - AutoTranslatedModelSerializer, OrganizationRelatedSerializer, serializers.ModelSerializer, ): @@ -430,16 +430,15 @@ def get_related_organizations(self) -> list[Organization]: return [self.instance.organization] if self.instance else [] -class ProjectCategorySuperLightSerializer( - AutoTranslatedModelSerializer, serializers.ModelSerializer -): +@auto_translated +class ProjectCategorySuperLightSerializer(serializers.ModelSerializer): class Meta: model = ProjectCategory fields = ["id", "slug", "name"] +@auto_translated class ProjectCategoryLightSerializer( - AutoTranslatedModelSerializer, OrganizationRelatedSerializer, serializers.ModelSerializer, ): @@ -462,8 +461,8 @@ def get_related_organizations(self) -> list[Organization]: return [ProjectCategory.objects.get(id=self.validated_data["id"]).organization] +@auto_translated class ProjectTemplateSerializer( - AutoTranslatedModelSerializer, OrganizationRelatedSerializer, serializers.ModelSerializer, ): @@ -491,9 +490,9 @@ class Meta: fields = read_only_fields +@auto_translated class TemplateSerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, OrganizationRelatedSerializer, serializers.ModelSerializer, ): @@ -562,8 +561,8 @@ def get_string_images_kwargs( } +@auto_translated class ProjectCategoryHierarchySerializer( - AutoTranslatedModelSerializer, OrganizationRelatedSerializer, serializers.ModelSerializer, ): @@ -607,9 +606,9 @@ def get_children(self, category: ProjectCategory) -> list[dict[str, str | int]]: ).data +@auto_translated class ProjectCategorySerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, OrganizationRelatedSerializer, serializers.ModelSerializer, ): diff --git a/apps/projects/serializers.py b/apps/projects/serializers.py index c14aa826..5364ad6d 100644 --- a/apps/projects/serializers.py +++ b/apps/projects/serializers.py @@ -43,7 +43,7 @@ ) from apps.skills.models import Tag from apps.skills.serializers import TagRelatedField, TagSerializer -from services.translator.serializers import AutoTranslatedModelSerializer +from services.translator.serializers import auto_translated from .exceptions import ( AddProjectToOrganizationPermissionError, @@ -70,9 +70,9 @@ from .utils import compute_project_changes, get_views_from_serializer +@auto_translated class BlogEntrySerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, OrganizationRelatedSerializer, ProjectRelatedSerializer, serializers.ModelSerializer, @@ -141,9 +141,9 @@ def get_string_images_kwargs( return {"project_id": instance.project.id} +@auto_translated class GoalSerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, OrganizationRelatedSerializer, ProjectRelatedSerializer, serializers.ModelSerializer, @@ -181,9 +181,8 @@ def get_related_project(self) -> Project | None: return None -class LocationProjectSerializer( - AutoTranslatedModelSerializer, serializers.ModelSerializer -): +@auto_translated +class LocationProjectSerializer(serializers.ModelSerializer): header_image = ImageSerializer(read_only=True) class Meta: @@ -191,6 +190,7 @@ class Meta: fields = ["id", "slug", "title", "purpose", "header_image"] +@auto_translated class LocationSerializer(ProjectRelatedSerializer, BaseLocationSerializer): string_images_forbid_fields: list[str] = ["title", "description"] @@ -223,17 +223,15 @@ def get_related_project(self) -> Project | None: return None -class ProjectSuperLightSerializer( - AutoTranslatedModelSerializer, serializers.ModelSerializer -): +@auto_translated +class ProjectSuperLightSerializer(serializers.ModelSerializer): class Meta: model = Project fields = ["id", "slug", "title"] -class ProjectLightSerializer( - AutoTranslatedModelSerializer, serializers.ModelSerializer -): +@auto_translated +class ProjectLightSerializer(serializers.ModelSerializer): categories = ProjectCategoryLightSerializer(many=True, read_only=True) header_image = ImageSerializer(read_only=True) is_followed = serializers.SerializerMethodField(read_only=True) @@ -512,9 +510,9 @@ def create(self, validated_data): } +@auto_translated class ProjectSerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, OrganizationRelatedSerializer, serializers.ModelSerializer, ): @@ -868,9 +866,9 @@ class Meta: ] +@auto_translated class ProjectMessageSerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, serializers.ModelSerializer, ): string_images_fields: list[str] = ["content"] @@ -923,9 +921,9 @@ def get_string_images_kwargs( return {"project_id": instance.project.id} +@auto_translated class ProjectTabSerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, serializers.ModelSerializer, ): string_images_fields: list[str] = ["description"] @@ -960,9 +958,9 @@ def get_string_images_kwargs( return {"project_id": instance.project.id} +@auto_translated class ProjectTabItemSerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, serializers.ModelSerializer, ): string_images_fields: list[str] = ["content"] diff --git a/apps/projects/tests/views/test_project_history.py b/apps/projects/tests/views/test_project_history.py index 025b53ed..a72e858c 100644 --- a/apps/projects/tests/views/test_project_history.py +++ b/apps/projects/tests/views/test_project_history.py @@ -485,7 +485,7 @@ def test_update_purpose_and_title(self): ) self.assertEqual(version["purpose"], payload["purpose"]) self.assertEqual(version["title"], payload["title"]) - self.assertEqual(version["history_change_reason"], "Updated: title + purpose") + self.assertEqual(version["history_change_reason"], "Updated: purpose + title") project.refresh_from_db() self.assertNotEqual(updated_at, project.updated_at) diff --git a/apps/projects/views.py b/apps/projects/views.py index 2d4b2722..4e7a5a5d 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -152,7 +152,8 @@ def perform_create(self, serializer: ProjectSerializer): def perform_update(self, serializer: ProjectSerializer): project = serializer.save() changes = serializer.validated_data - update_change_reason(project, f"Updated: {' + '.join(changes.keys())}"[:100]) + fields = sorted(changes.keys()) + update_change_reason(project, f"Updated: {' + '.join(fields)}"[:100]) if ( settings.ENABLE_CACHE and changes.get("publication_status") diff --git a/apps/search/serializers.py b/apps/search/serializers.py index b3c9df29..026006be 100644 --- a/apps/search/serializers.py +++ b/apps/search/serializers.py @@ -10,14 +10,13 @@ from apps.files.serializers import ImageSerializer from apps.organizations.serializers import ProjectCategoryLightSerializer from apps.projects.models import Project -from services.translator.serializers import AutoTranslatedModelSerializer +from services.translator.serializers import auto_translated from .models import SearchObject -class ProjectSearchSerializer( - AutoTranslatedModelSerializer, serializers.ModelSerializer -): +@auto_translated +class ProjectSearchSerializer(serializers.ModelSerializer): categories = ProjectCategoryLightSerializer(many=True, read_only=True) header_image = ImageSerializer(read_only=True) is_followed = serializers.SerializerMethodField(read_only=True) diff --git a/apps/skills/serializers.py b/apps/skills/serializers.py index b47036a2..47ffaa7e 100644 --- a/apps/skills/serializers.py +++ b/apps/skills/serializers.py @@ -10,13 +10,12 @@ HiddenPrimaryKeyRelatedField, UserMultipleIdRelatedField, ) -from apps.commons.serializers import ( - LazySerializer, - StringsImagesSerializer, - TranslatedModelSerializer, -) +from apps.commons.serializers import LazySerializer, StringsImagesSerializer from apps.commons.utils import remove_images_text -from services.translator.serializers import AutoTranslatedModelSerializer +from services.translator.serializers import ( + auto_translated, + external_auto_translated, +) from .exceptions import ( TagDescriptionTooLongError, @@ -28,9 +27,8 @@ from .models import Mentoring, Skill, Tag, TagClassification -class TagClassificationLightSerializer( - AutoTranslatedModelSerializer, serializers.ModelSerializer -): +@auto_translated +class TagClassificationLightSerializer(serializers.ModelSerializer): organization = serializers.SlugRelatedField(read_only=True, slug_field="code") class Meta: @@ -46,9 +44,9 @@ class Meta: fields = read_only_fields +@auto_translated class TagClassificationSerializer( StringsImagesSerializer, - AutoTranslatedModelSerializer, serializers.ModelSerializer, ): string_images_forbid_fields: list[str] = ["title", "description"] @@ -178,7 +176,8 @@ def create(self, validated_data): return tag_classification -class TagSerializer(TranslatedModelSerializer): +@external_auto_translated +class TagSerializer(serializers.ModelSerializer): mentors_count = serializers.IntegerField(required=False, read_only=True) mentorees_count = serializers.IntegerField(required=False, read_only=True) highlight = serializers.JSONField(required=False, read_only=True) diff --git a/locale/ca/LC_MESSAGES/django.po b/locale/ca/LC_MESSAGES/django.po index 6cea30d1..0f6827a8 100644 --- a/locale/ca/LC_MESSAGES/django.po +++ b/locale/ca/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-18 10:17+0100\n" +"POT-Creation-Date: 2026-03-19 20:02+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -176,7 +176,7 @@ msgstr "Estat de publicació desconegut" msgid "Unknown publication status '{publication_status}'" msgstr "Estat de publicació desconegut '{publication_status}'" -#: apps/commons/fields.py:31 apps/skills/serializers.py:113 +#: apps/commons/fields.py:31 apps/skills/serializers.py:111 msgid "This field is required." msgstr "Aquest camp és obligatori." @@ -185,12 +185,12 @@ msgstr "Aquest camp és obligatori." msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "Id no vàlid \"{user_id}\" - l'objecte no existeix." -#: apps/commons/fields.py:34 apps/skills/serializers.py:118 +#: apps/commons/fields.py:34 apps/skills/serializers.py:116 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "Tipus incorrecte. S'esperava un valor str, s'ha rebut {data_type}." -#: apps/commons/serializers.py:241 +#: apps/commons/serializers.py:210 msgid "The value must be between -90 and 90." msgstr "" @@ -1633,7 +1633,7 @@ msgstr "El títol de l'etiqueta ha de tenir 50 caràcters o menys" msgid "Tag description must be 500 characters or less" msgstr "La descripció de l'etiqueta ha de tenir 500 caràcters o menys" -#: apps/skills/serializers.py:115 +#: apps/skills/serializers.py:113 #, python-brace-format msgid "Invalid id \"{tag_classification_id}\" - object does not exist." msgstr "Id no vàlid \"{tag_classification_id}\" - l'objecte no existeix." diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index b57f3fd4..3dc34069 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-18 10:17+0100\n" +"POT-Creation-Date: 2026-03-19 20:02+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -178,7 +178,7 @@ msgstr "Unbekannter Veröffentlichungsstatus" msgid "Unknown publication status '{publication_status}'" msgstr "Unbekannter Veröffentlichungsstatus '{publication_status}'" -#: apps/commons/fields.py:31 apps/skills/serializers.py:113 +#: apps/commons/fields.py:31 apps/skills/serializers.py:111 msgid "This field is required." msgstr "Dieses Feld ist erforderlich." @@ -187,13 +187,13 @@ msgstr "Dieses Feld ist erforderlich." msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "Ungültige ID \"{user_id}\" – Objekt existiert nicht." -#: apps/commons/fields.py:34 apps/skills/serializers.py:118 +#: apps/commons/fields.py:34 apps/skills/serializers.py:116 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "" "Falscher Typ. Erwartet wurde ein String-Wert, erhalten wurde {data_type}." -#: apps/commons/serializers.py:241 +#: apps/commons/serializers.py:210 msgid "The value must be between -90 and 90." msgstr "" @@ -1652,7 +1652,7 @@ msgstr "Der Tag-Titel darf maximal 50 Zeichen lang sein" msgid "Tag description must be 500 characters or less" msgstr "Die Tag-Beschreibung darf maximal 500 Zeichen lang sein" -#: apps/skills/serializers.py:115 +#: apps/skills/serializers.py:113 #, python-brace-format msgid "Invalid id \"{tag_classification_id}\" - object does not exist." msgstr "Ungültige ID \"{tag_classification_id}\" – Objekt existiert nicht." diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index de65f8d1..11479015 100644 --- a/locale/en/LC_MESSAGES/django.po +++ b/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-18 10:17+0100\n" +"POT-Creation-Date: 2026-03-19 20:02+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -154,7 +154,7 @@ msgstr "" msgid "Unknown publication status '{publication_status}'" msgstr "" -#: apps/commons/fields.py:31 apps/skills/serializers.py:113 +#: apps/commons/fields.py:31 apps/skills/serializers.py:111 msgid "This field is required." msgstr "" @@ -163,12 +163,12 @@ msgstr "" msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "" -#: apps/commons/fields.py:34 apps/skills/serializers.py:118 +#: apps/commons/fields.py:34 apps/skills/serializers.py:116 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "" -#: apps/commons/serializers.py:241 +#: apps/commons/serializers.py:210 msgid "The value must be between -90 and 90." msgstr "" @@ -1289,7 +1289,7 @@ msgstr "" msgid "Tag description must be 500 characters or less" msgstr "" -#: apps/skills/serializers.py:115 +#: apps/skills/serializers.py:113 #, python-brace-format msgid "Invalid id \"{tag_classification_id}\" - object does not exist." msgstr "" diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po index 6f6c5efb..c8015653 100644 --- a/locale/es/LC_MESSAGES/django.po +++ b/locale/es/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-18 10:17+0100\n" +"POT-Creation-Date: 2026-03-19 20:02+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -176,7 +176,7 @@ msgstr "Estado de publicación desconocido" msgid "Unknown publication status '{publication_status}'" msgstr "Estado de publicación desconocido '{publication_status}'" -#: apps/commons/fields.py:31 apps/skills/serializers.py:113 +#: apps/commons/fields.py:31 apps/skills/serializers.py:111 msgid "This field is required." msgstr "Este campo es obligatorio." @@ -185,13 +185,13 @@ msgstr "Este campo es obligatorio." msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "ID no válido \"{user_id}\" - el objeto no existe." -#: apps/commons/fields.py:34 apps/skills/serializers.py:118 +#: apps/commons/fields.py:34 apps/skills/serializers.py:116 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "" "Tipo incorrecto. Se esperaba un valor de cadena, se recibió {data_type}." -#: apps/commons/serializers.py:241 +#: apps/commons/serializers.py:210 msgid "The value must be between -90 and 90." msgstr "" @@ -1632,7 +1632,7 @@ msgstr "El título de la etiqueta debe tener 50 caracteres o menos" msgid "Tag description must be 500 characters or less" msgstr "La descripción de la etiqueta debe tener 500 caracteres o menos" -#: apps/skills/serializers.py:115 +#: apps/skills/serializers.py:113 #, python-brace-format msgid "Invalid id \"{tag_classification_id}\" - object does not exist." msgstr "ID no válida \"{tag_classification_id}\" - el objeto no existe." diff --git a/locale/et/LC_MESSAGES/django.po b/locale/et/LC_MESSAGES/django.po index 3f34af90..3b85a0cd 100644 --- a/locale/et/LC_MESSAGES/django.po +++ b/locale/et/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-18 10:17+0100\n" +"POT-Creation-Date: 2026-03-19 20:02+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -175,7 +175,7 @@ msgstr "Tundmatu avaldamise olek" msgid "Unknown publication status '{publication_status}'" msgstr "Tundmatu avaldamise olek '{publication_status}'" -#: apps/commons/fields.py:31 apps/skills/serializers.py:113 +#: apps/commons/fields.py:31 apps/skills/serializers.py:111 msgid "This field is required." msgstr "See väli on kohustuslik." @@ -184,12 +184,12 @@ msgstr "See väli on kohustuslik." msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "Vigane ID \"{user_id}\" - objekti ei eksisteeri." -#: apps/commons/fields.py:34 apps/skills/serializers.py:118 +#: apps/commons/fields.py:34 apps/skills/serializers.py:116 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "Vale tüüp. Oodati stringi väärtust, saadi {data_type}." -#: apps/commons/serializers.py:241 +#: apps/commons/serializers.py:210 msgid "The value must be between -90 and 90." msgstr "" @@ -1610,7 +1610,7 @@ msgstr "Sildi pealkiri peab olema 50 tähemärki või vähem" msgid "Tag description must be 500 characters or less" msgstr "Sildi kirjeldus peab olema 500 tähemärki või vähem" -#: apps/skills/serializers.py:115 +#: apps/skills/serializers.py:113 #, python-brace-format msgid "Invalid id \"{tag_classification_id}\" - object does not exist." msgstr "Vigane ID \"{tag_classification_id}\" - objekti ei eksisteeri." diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index cc5805df..d8183afb 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-18 10:17+0100\n" +"POT-Creation-Date: 2026-03-19 20:02+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -178,7 +178,7 @@ msgstr "Statut de publication inconnu" msgid "Unknown publication status '{publication_status}'" msgstr "Statut de publication inconnu '{publication_status}'" -#: apps/commons/fields.py:31 apps/skills/serializers.py:113 +#: apps/commons/fields.py:31 apps/skills/serializers.py:111 msgid "This field is required." msgstr "Ce champ est obligatoire." @@ -187,12 +187,12 @@ msgstr "Ce champ est obligatoire." msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "identifiant invalide \"{user_id}\" - cet objet n'existe pas." -#: apps/commons/fields.py:34 apps/skills/serializers.py:118 +#: apps/commons/fields.py:34 apps/skills/serializers.py:116 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "Type incorrect. Valeur str attendue, {data_type} reçue." -#: apps/commons/serializers.py:241 +#: apps/commons/serializers.py:210 msgid "The value must be between -90 and 90." msgstr "" @@ -1631,7 +1631,7 @@ msgstr "Le titre du tag doit comporter 50 caractères ou moins" msgid "Tag description must be 500 characters or less" msgstr "La description du tag doit comporter 500 caractères ou moins" -#: apps/skills/serializers.py:115 +#: apps/skills/serializers.py:113 #, python-brace-format msgid "Invalid id \"{tag_classification_id}\" - object does not exist." msgstr "" diff --git a/locale/nl/LC_MESSAGES/django.po b/locale/nl/LC_MESSAGES/django.po index c162123c..5d659298 100644 --- a/locale/nl/LC_MESSAGES/django.po +++ b/locale/nl/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-18 10:17+0100\n" +"POT-Creation-Date: 2026-03-19 20:02+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -178,7 +178,7 @@ msgstr "Onbekende publicatiestatus" msgid "Unknown publication status '{publication_status}'" msgstr "Onbekende publicatiestatus '{publication_status}'" -#: apps/commons/fields.py:31 apps/skills/serializers.py:113 +#: apps/commons/fields.py:31 apps/skills/serializers.py:111 msgid "This field is required." msgstr "Dit veld is verplicht." @@ -187,12 +187,12 @@ msgstr "Dit veld is verplicht." msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "Ongeldige id \"{user_id}\" - object bestaat niet." -#: apps/commons/fields.py:34 apps/skills/serializers.py:118 +#: apps/commons/fields.py:34 apps/skills/serializers.py:116 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "Onjuist type. Verwachte stringwaarde, ontvangen {data_type}." -#: apps/commons/serializers.py:241 +#: apps/commons/serializers.py:210 msgid "The value must be between -90 and 90." msgstr "" @@ -1643,7 +1643,7 @@ msgstr "Tagtitel moet 50 tekens of minder zijn" msgid "Tag description must be 500 characters or less" msgstr "Tagbeschrijving moet 500 tekens of minder zijn" -#: apps/skills/serializers.py:115 +#: apps/skills/serializers.py:113 #, python-brace-format msgid "Invalid id \"{tag_classification_id}\" - object does not exist." msgstr "Ongeldige id \"{tag_classification_id}\" - object bestaat niet." diff --git a/services/crisalid/serializers.py b/services/crisalid/serializers.py index 73848494..8da41afc 100644 --- a/services/crisalid/serializers.py +++ b/services/crisalid/serializers.py @@ -3,7 +3,7 @@ from apps.accounts.models import ProjectUser from apps.commons.fields import PrivacySettingProtectedMethodField from services.crisalid.models import Document, Identifier, Researcher -from services.translator.serializers import AutoTranslatedModelSerializer +from services.translator.serializers import auto_translated class ProjectUserMinimalSerializer(serializers.ModelSerializer): @@ -57,12 +57,14 @@ class Meta(ResearcherSerializer.Meta): fields = ("id", "user", "display_name") -class DocumentLightSerializer(AutoTranslatedModelSerializer): +@auto_translated +class DocumentLightSerializer(serializers.ModelSerializer): class Meta: model = Document fields = ("title", "publication_date", "document_type") +@auto_translated class DocumentSerializer(DocumentLightSerializer): contributors = ResearcherDocumentsSerializer(many=True) identifiers = IdentifierSerializer(many=True) diff --git a/services/translator/serializers.py b/services/translator/serializers.py index 285150ad..dfb31535 100644 --- a/services/translator/serializers.py +++ b/services/translator/serializers.py @@ -1,40 +1,88 @@ from django.conf import settings +from modeltranslation.manager import get_translatable_fields_for_model from rest_framework import serializers +from rest_framework.serializers import ALL_FIELDS +from services.translator.mixins import HasAutoTranslatedFields -class AutoTranslatedModelSerializer(serializers.ModelSerializer): - """ - Automatically include translations fields for models with `HasAutoTranslatedFields` mixin. + +def auto_translated(cls: serializers.ModelSerializer) -> serializers.ModelSerializer: + """Automatically include translations fields for models with `HasAutoTranslatedFields` mixin. Because these are automatically generated, they are read-only. """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # Add translated fields to the read_only_fields - fields = getattr(self.Meta, "fields", []) - read_only_fields = getattr(self.Meta, "read_only_fields", []) - translated_fields = getattr(self.Meta.model, "_auto_translated_fields", []) - for field in fields: - if field in translated_fields: - read_only_fields.append(f"{field}_detected_language") - for lang in settings.REQUIRED_LANGUAGES: - read_only_fields.append(f"{field}_{lang}") - self.Meta.read_only_fields = read_only_fields - - def get_field_names(self, declared_fields, info): - fields = super().get_field_names(declared_fields, info) - # Get HasAutoTranslatedFields mixin _auto_translated_fields - translated_fields = getattr(self.Meta.model, "_auto_translated_fields", []) - all_fields = [] - for field in fields: - all_fields.append(field) - if field in translated_fields: - all_fields.append(f"{field}_detected_language") - for lang in settings.REQUIRED_LANGUAGES: - all_fields.append(f"{field}_{lang}") - return all_fields - - class Meta: - model = None - read_only_fields = [] + model = cls.Meta.model + + assert issubclass( + model, HasAutoTranslatedFields + ), f"You model ({model}) need to inherit 'HasAutoTranslatedFields'" + + # model translated field name + auto_translated_fields = model._auto_translated_fields + + fields_available = [] + for name in cls().get_fields(): + if name in auto_translated_fields: + fields_available.append(name) + + # not fields is needed + if not fields_available: + return cls + + fields_to_add = [f"{field}_detected_language" for field in fields_available] + # generates all fields + for field in fields_available: + fields_to_add.extend(f"{field}_{lang}" for lang in settings.REQUIRED_LANGUAGES) + + # set all fields in read_only (use set to avoid duplicated refered) + read_only_fields = getattr(cls.Meta, "read_only_fields", []) + cls.Meta.read_only_fields = tuple(set(read_only_fields) | set(fields_to_add)) + + # set all fields in fields + fields = getattr(cls.Meta, "fields", None) + # if fields is set (can be None for exlucde content) and field is not "__all__" value + # (use set to avoid duplicated redered field) + if fields and fields != ALL_FIELDS: + cls.Meta.fields = tuple(set(fields) | set(fields_to_add)) + + return cls + + +def external_auto_translated( + cls: serializers.ModelSerializer, +) -> serializers.ModelSerializer: + """Automatically include translations fields for models with from `modeltranslation` lib. + + all field generated is not read-only (cant be set from serializer). + """ + + model = cls.Meta.model + + auto_translated_fields = get_translatable_fields_for_model(model) + assert ( + auto_translated_fields is not None + ), f"You model ({model}) need to register from 'modeltranslation'" + + fields_available = [] + for name in cls().get_fields(): + if name in auto_translated_fields: + fields_available.append(name) + + # not fields is needed + if not fields_available: + return cls + + fields_to_add = [] + # generates all fields + for field in fields_available: + fields_to_add.extend(f"{field}_{lang}" for lang in settings.REQUIRED_LANGUAGES) + + # set all fields in fields + fields = getattr(cls.Meta, "fields", None) + # if fields is set (can be None for exlucde content) and field is not "__all__" value + # (use set to avoid duplicated redered field) + if fields and fields != ALL_FIELDS: + cls.Meta.fields = tuple(set(fields) | set(fields_to_add)) + + return cls