diff --git a/apps/accounts/models.py b/apps/accounts/models.py index ef05894c..e526afd1 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -30,6 +30,7 @@ HasMultipleIDs, HasOwner, HasPermissionsSetup, + HasRelatedLocationContent, HasRelatedModules, OrganizationRelated, ) @@ -44,7 +45,9 @@ from services.translator.mixins import HasAutoTranslatedFields -class PeopleGroupLocation(OrganizationRelated, AbstractLocation): +class PeopleGroupLocation( + OrganizationRelated, HasRelatedLocationContent, AbstractLocation +): """base location for group""" people_group = models.ForeignKey( @@ -53,6 +56,10 @@ class PeopleGroupLocation(OrganizationRelated, AbstractLocation): related_name="locations", ) + @classmethod + def get_related_content(cls): + return cls.people_group.field.name + def get_related_organizations(self) -> list["Organization"]: return [self.people_group.organization] diff --git a/apps/accounts/views.py b/apps/accounts/views.py index f522435f..9e94705a 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -46,6 +46,7 @@ MultipleIDViewsetMixin, NestedOrganizationViewMixins, NestedPeopleGroupViewMixins, + QuerySerializersMixin, ) from apps.files.models import Image from apps.files.views import ImageStorageView @@ -90,6 +91,7 @@ PeopleGroupRemoveFeaturedProjectsSerializer, PeopleGroupRemoveTeamMembersSerializer, PeopleGroupSerializer, + PeopleGroupSuperLightSerializer, PrivacySettingsSerializer, UserAdminListSerializer, UserLighterSerializer, @@ -519,7 +521,9 @@ def refresh_keycloak_actions_link(self, request, *args, **kwargs): ) -class PeopleGroupViewSet(MultipleIDViewsetMixin, viewsets.ModelViewSet): +class PeopleGroupViewSet( + QuerySerializersMixin, MultipleIDViewsetMixin, viewsets.ModelViewSet +): queryset = PeopleGroup.objects.all() serializer_class = PeopleGroupSerializer filterset_class = PeopleGroupFilter @@ -531,6 +535,10 @@ class PeopleGroupViewSet(MultipleIDViewsetMixin, viewsets.ModelViewSet): OrderingFilter, ) multiple_lookup_fields = [(PeopleGroup, "id")] + query_serializers = { + "light": PeopleGroupLightSerializer, + "superlight": PeopleGroupSuperLightSerializer, + } def get_permissions(self): codename = map_action_to_permission(self.action, "peoplegroup") @@ -557,9 +565,8 @@ def get_queryset(self) -> QuerySet: return PeopleGroup.objects.none() def get_serializer_class(self): - if self.action == "list": - return PeopleGroupLightSerializer - return self.serializer_class + query = "light" if self.action == "list" else None + return super().get_serializer_class(query) def get_serializer_context(self): context = super().get_serializer_context() diff --git a/apps/commons/mixins.py b/apps/commons/mixins.py index e4c65ecf..eb7ee23e 100644 --- a/apps/commons/mixins.py +++ b/apps/commons/mixins.py @@ -491,3 +491,8 @@ def similars(self, threshold: float = 0.15) -> QuerySet[Self]: pk=self.pk ) return type(self).objects.none() + + +class HasRelatedLocationContent: + def get_related_content(self): + raise NotImplementedError diff --git a/apps/commons/views.py b/apps/commons/views.py index d1ff0ca8..71937a9b 100644 --- a/apps/commons/views.py +++ b/apps/commons/views.py @@ -2,7 +2,8 @@ from django.db.models import QuerySet from django.shortcuts import get_object_or_404 -from rest_framework import mixins, viewsets +from drf_spectacular.utils import OpenApiParameter as _OpenApiParameter +from rest_framework import mixins, serializers, viewsets from rest_framework.response import Response from rest_framework.settings import api_settings @@ -193,3 +194,28 @@ def initial(self, request, *args, **kwargs): ) super().initial(request, *args, **kwargs) + + +class QuerySerializersMixin: + """return specified serializer from queryparams""" + + query_serializers: dict[str, serializers.Serializer] = {} + + def get_serializer_class(self, query=None) -> serializers.Serializer: + query = query or self.request.query_params.get("serializer") + serializer = None + if query: + serializer = self.query_serializers.get(query) + + return serializer or super().get_serializer_class() + + @classmethod + def OpenApiParameter( # noqa: N802 + cls, serializers: dict[str, serializers.Serializer] + ) -> _OpenApiParameter: + return _OpenApiParameter( + name="serializer", + description="change output serializer", + required=False, + enum=serializers.keys(), + ) diff --git a/apps/newsfeed/models.py b/apps/newsfeed/models.py index 51ecb2a0..9c6be8a5 100644 --- a/apps/newsfeed/models.py +++ b/apps/newsfeed/models.py @@ -3,7 +3,11 @@ from django.db import models from apps.commons.enums import Language -from apps.commons.mixins import HasOwner, OrganizationRelated +from apps.commons.mixins import ( + HasOwner, + HasRelatedLocationContent, + OrganizationRelated, +) from apps.projects.models import AbstractLocation from services.translator.mixins import HasAutoTranslatedFields @@ -62,7 +66,7 @@ class NewsfeedType(models.TextChoices): ) -class NewsLocation(AbstractLocation): +class NewsLocation(HasRelatedLocationContent, AbstractLocation): news = models.OneToOneField( "newsfeed.News", related_name="location", @@ -71,6 +75,10 @@ class NewsLocation(AbstractLocation): blank=True, ) + @classmethod + def get_related_content(cls): + return cls.news.field.name + def get_related_organizations(self) -> list["Organization"]: """Return the organizations related to this model.""" return self.news.get_related_organizations() @@ -199,7 +207,7 @@ def is_owned_by(self, user: "ProjectUser") -> bool: return self.owner == user -class EventLocation(AbstractLocation): +class EventLocation(HasRelatedLocationContent, AbstractLocation): event = models.OneToOneField( "newsfeed.Event", related_name="location", @@ -208,6 +216,10 @@ class EventLocation(AbstractLocation): blank=False, ) + @classmethod + def get_related_content(cls): + return cls.event.field.name + def get_related_organizations(self) -> list["Organization"]: """Return the organizations related to this model.""" return self.event.get_related_organizations() diff --git a/apps/newsfeed/views.py b/apps/newsfeed/views.py index 7598dacb..9d4b169c 100644 --- a/apps/newsfeed/views.py +++ b/apps/newsfeed/views.py @@ -13,7 +13,11 @@ from apps.accounts.permissions import HasBasePermission from apps.commons.permissions import ReadOnly from apps.commons.utils import map_action_to_permission -from apps.commons.views import ListViewSet, NestedOrganizationViewMixins +from apps.commons.views import ( + ListViewSet, + NestedOrganizationViewMixins, + QuerySerializersMixin, +) from apps.files.models import Image from apps.files.views import ImageStorageView from apps.organizations.permissions import HasOrganizationPermission @@ -22,9 +26,11 @@ from .filters import EventFilter, InstructionFilter, NewsFilter from .models import Event, Instruction, News, Newsfeed from .serializers import ( + EventLightSerializer, EventSerializer, InstructionSerializer, NewsfeedSerializer, + NewsLightSerializer, NewsSerializer, ) @@ -119,7 +125,7 @@ def get_queryset(self): return self.merge_querysets(announcements, news, projects) -class NewsViewSet(viewsets.ModelViewSet): +class NewsViewSet(QuerySerializersMixin, viewsets.ModelViewSet): """Main endpoints for news.""" serializer_class = NewsSerializer @@ -128,6 +134,7 @@ class NewsViewSet(viewsets.ModelViewSet): ordering_fields = ("updated_at", "created_at", "publication_date") lookup_field = "id" lookup_value_regex = "[^/]+" + query_serializers = {"light": NewsLightSerializer} def get_permissions(self): codename = map_action_to_permission(self.action, "news") @@ -319,7 +326,7 @@ def add_image_to_model(self, image): return None -class EventViewSet(viewsets.ModelViewSet): +class EventViewSet(QuerySerializersMixin, viewsets.ModelViewSet): """Main endpoints for projects.""" serializer_class = EventSerializer @@ -328,6 +335,7 @@ class EventViewSet(viewsets.ModelViewSet): ordering_fields = ("start_date", "end_date", "updated_at", "created_at") lookup_field = "id" lookup_value_regex = "[^/]+" + query_serializers = {"light": EventLightSerializer} def get_permissions(self): codename = map_action_to_permission(self.action, "event") diff --git a/apps/organizations/views.py b/apps/organizations/views.py index a4f07424..ab3a50fe 100644 --- a/apps/organizations/views.py +++ b/apps/organizations/views.py @@ -20,7 +20,6 @@ from apps.accounts.permissions import HasBasePermission from apps.accounts.serializers import ( PeopleGroupHierarchySerializer, - PeopleGroupLocationSuperLightSerializer, UserSerializer, ) from apps.commons.cache import clear_cache_with_key, redis_cache_view @@ -35,12 +34,12 @@ from apps.files.views import ImageStorageView from apps.modules.group import PeopleGroupModules from apps.newsfeed.models import EventLocation, NewsLocation -from apps.newsfeed.serializers import ( - EventLocationSerializerLight, - NewsLocationSerializerLight, -) from apps.projects.models import Location, Project -from apps.projects.serializers import LocationSerializer, ProjectLightSerializer +from apps.projects.serializers import ( + GeneralLocationSerializer, + ProjectLightSerializer, +) +from apps.projects.utils import annotate_queryset_location from .exceptions import ( MissingLifeStatusParameterError, @@ -736,31 +735,35 @@ def get(self, request): class GeneralLocationView(NestedOrganizationViewMixins, viewsets.GenericViewSet): http_method_names = ["get", "list"] - def list(self, request, *args, **kwargs): - qs_project = ( - request.user.get_project_related_queryset(Location.objects) - .select_related("project") - .filter(project__organizations__in=self.organizations) + def get_queryset_project(self) -> QuerySet[Location]: + return self.request.user.get_project_related_queryset(Location.objects).filter( + project__organizations__in=self.organizations ) - qs_group = request.user.get_people_group_related_queryset( + def get_queryset_groups(self) -> QuerySet[PeopleGroupLocation]: + return self.request.user.get_people_group_related_queryset( PeopleGroupLocation.objects.filter( people_group__organization__in=self.organizations ) - ).select_related("people_group") + ) - qs_news = request.user.get_news_related_queryset( + def get_queryset_news(self) -> QuerySet[NewsLocation]: + return self.request.user.get_news_related_queryset( NewsLocation.objects.filter(news__organization__in=self.organizations) - ).select_related("news") + ) - qs_event = request.user.get_event_related_queryset( + def get_queryset_event(self) -> QuerySet[EventLocation]: + return self.request.user.get_event_related_queryset( EventLocation.objects.filter(event__organization__in=self.organizations) - ).select_related("event") - - data = { - "groups": PeopleGroupLocationSuperLightSerializer(qs_group, many=True).data, - "projects": LocationSerializer(qs_project, many=True).data, - "news": NewsLocationSerializerLight(qs_news, many=True).data, - "event": EventLocationSerializerLight(qs_event, many=True).data, - } + ) + + def list(self, request, *args, **kwargs): + qs_project = self.get_queryset_project() + qs_groups = self.get_queryset_groups() + qs_news = self.get_queryset_news() + qs_event = self.get_queryset_event() + + qs = annotate_queryset_location(qs_project, qs_groups, qs_news, qs_event) + + data = GeneralLocationSerializer(list(qs), many=True).data return Response(data, status=status.HTTP_200_OK) diff --git a/apps/projects/models.py b/apps/projects/models.py index e0ab3d80..d3ca08c5 100644 --- a/apps/projects/models.py +++ b/apps/projects/models.py @@ -24,6 +24,7 @@ HasMultipleIDs, HasOwner, HasPermissionsSetup, + HasRelatedLocationContent, HasRelatedModules, ProjectRelated, ) @@ -901,7 +902,7 @@ class Meta: # TODO(remi): rename to ProjectLocation ? -class Location(ProjectRelated, AbstractLocation): +class Location(ProjectRelated, HasRelatedLocationContent, AbstractLocation): """A project location on Earth. Attributes @@ -916,6 +917,10 @@ class Location(ProjectRelated, AbstractLocation): Project, on_delete=models.CASCADE, related_name="locations" ) + @classmethod + def get_related_content(cls): + return cls.project.field.name + def get_related_project(self) -> Optional["Project"]: """Return the projects related to this model.""" return self.project diff --git a/apps/projects/serializers.py b/apps/projects/serializers.py index 223c50de..e9177de5 100644 --- a/apps/projects/serializers.py +++ b/apps/projects/serializers.py @@ -40,7 +40,10 @@ ) from apps.skills.models import Tag from apps.skills.serializers import TagRelatedField -from services.translator.serializers import auto_translated +from services.translator.serializers import ( + auto_translated, + generate_translated_fields, +) from .exceptions import ( AddProjectToOrganizationPermissionError, @@ -963,3 +966,15 @@ def get_string_images_kwargs( "project_id": instance.tab.project.id, "tab_id": instance.tab.id, } + + +@generate_translated_fields(("title", "description")) +class GeneralLocationSerializer(serializers.Serializer): + id = serializers.IntegerField() + title = serializers.CharField() + description = serializers.CharField() + content_id = serializers.CharField() + content_type = serializers.CharField() + lat = serializers.FloatField() + lng = serializers.FloatField() + type = serializers.CharField() diff --git a/apps/projects/tests/views/test_read_location.py b/apps/projects/tests/views/test_read_location.py index 00a17f36..992f6779 100644 --- a/apps/projects/tests/views/test_read_location.py +++ b/apps/projects/tests/views/test_read_location.py @@ -111,12 +111,15 @@ def test_list_project_location(self, role, retrieved_locations): reverse("General-location-list", args=(self.organization.code,)) ) self.assertEqual(response.status_code, status.HTTP_200_OK) - content = response.json() + locations = response.json() # projects (from organization, not organization_other) - self.assertEqual(len(content["projects"]), len(retrieved_locations)) self.assertSetEqual( - {a["id"] for a in content["projects"]}, + { + location["id"] + for location in locations + if location["content_type"] == "project" + }, {a.id for a in [self.locations[a] for a in retrieved_locations]}, ) @@ -141,11 +144,14 @@ def test_list_group_location(self, role, retrieved_locations): reverse("General-location-list", args=(self.organization.code,)) ) self.assertEqual(response.status_code, status.HTTP_200_OK) - content = response.json() + locations = response.json() - # groups (from organization, not organization_other) - self.assertEqual(len(content["groups"]), len(retrieved_locations)) + # projects (from organization, not organization_other) self.assertSetEqual( - {a["id"] for a in content["groups"]}, + { + location["id"] + for location in locations + if location["content_type"] == "people_group" + }, {a.id for a in [self.locations_group[a] for a in retrieved_locations]}, ) diff --git a/apps/projects/utils.py b/apps/projects/utils.py index 367e62a6..af64556b 100644 --- a/apps/projects/utils.py +++ b/apps/projects/utils.py @@ -1,9 +1,12 @@ from typing import Any, TypeVar +from django.db.models import CharField, QuerySet, Value +from django.db.models.functions import Cast from rest_framework import serializers from rest_framework.utils import model_meta from apps.organizations.models import Organization +from services.translator.serializers import prefix_fields_langs from .models import Project @@ -61,3 +64,34 @@ def compute_project_changes( changes[attr] = (old, new) return changes + + +def annotate_queryset_location(*querysets: QuerySet) -> QuerySet: + """annoate queryset for lazy load linked elements""" + + all_qs: QuerySet = None + fields = ( + "id", + "lat", + "lng", + "type", + "content_id", + "content_type", + "title", + "description", + # add generate field text + *prefix_fields_langs(("title", "description")), + ) + + for queryset in querysets: + model = queryset.model + content = model.get_related_content() + qs = queryset.annotate( + # cast linked object to string (project is slug so string, but news/events is pk so int) + content_id=Cast(f"{content}_id", output_field=CharField()), + content_type=Value(content), + ).values(*fields) + + all_qs = qs if all_qs is None else all_qs.union(qs) + + return all_qs diff --git a/apps/projects/views.py b/apps/projects/views.py index f8400eb9..c0d273f4 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -1,4 +1,3 @@ -import enum import uuid from django.apps import apps @@ -26,7 +25,11 @@ from apps.commons.cache import clear_cache_with_key, redis_cache_view from apps.commons.permissions import IsOwner, ReadOnly from apps.commons.utils import map_action_to_permission -from apps.commons.views import MultipleIDViewsetMixin, NestedProjectViewMixins +from apps.commons.views import ( + MultipleIDViewsetMixin, + NestedProjectViewMixins, + QuerySerializersMixin, +) from apps.files.models import Image from apps.files.views import ImageStorageView from apps.notifications.tasks import ( @@ -77,12 +80,11 @@ ) -class ProjectViewSet(MultipleIDViewsetMixin, viewsets.ModelViewSet): +class ProjectViewSet( + QuerySerializersMixin, MultipleIDViewsetMixin, viewsets.ModelViewSet +): """Main endpoints for projects.""" - class InfoDetails(enum.Enum): - SUMMARY = "summary" - serializer_class = ProjectSerializer filter_backends = [DjangoFilterBackend, OrderingFilter] filterset_class = ProjectFilter @@ -90,6 +92,10 @@ class InfoDetails(enum.Enum): lookup_field = "id" lookup_value_regex = "[^/]+" multiple_lookup_fields = [(Project, "id")] + query_serializers = { + "light": ProjectLightSerializer, + "superlight": ProjectSuperLightSerializer, + } def get_permissions(self): codename = map_action_to_permission(self.action, "project") @@ -116,13 +122,8 @@ def get_queryset(self) -> QuerySet: ) def get_serializer_class(self): - is_summary = ( - self.request.query_params.get("info_details") - == ProjectViewSet.InfoDetails.SUMMARY - ) - if self.action == "list" or is_summary: - return ProjectLightSerializer - return self.serializer_class + query = "light" if self.action == "list" else None + return super().get_serializer_class(query) def get_serializer_context(self): """Adds request to the serializer's context.""" @@ -157,15 +158,7 @@ def perform_destroy(self, instance): super().perform_destroy(instance) @extend_schema( - parameters=[ - OpenApiParameter( - name="info_details", - description='set this parameter to "summary" to get less details ' - "about the project", - required=False, - type=str, - ) - ] + parameters=[QuerySerializersMixin.OpenApiParameter(query_serializers)] ) def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) diff --git a/keycloak-lpi-theme b/keycloak-lpi-theme index 1159011d..2637cafc 160000 --- a/keycloak-lpi-theme +++ b/keycloak-lpi-theme @@ -1 +1 @@ -Subproject commit 1159011d5b053de9bf45bee944da312f3383008d +Subproject commit 2637cafc89ae3c3ab6c8ed514666a8219101ab82 diff --git a/locale/ca/LC_MESSAGES/django.po b/locale/ca/LC_MESSAGES/django.po index 1426eba8..37189d9d 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-05-27 10:40+0200\n" +"POT-Creation-Date: 2026-06-18 15:40+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -110,7 +110,7 @@ msgstr "No pots assignar aquest rol a un usuari" msgid "You cannot assign this role to a user : {role}" msgstr "No pots assignar aquest rol a un usuari: {role}" -#: apps/accounts/models.py:161 apps/projects/models.py:169 +#: apps/accounts/models.py:168 apps/projects/models.py:170 msgid "visibility" msgstr "visibilitat" @@ -1568,23 +1568,23 @@ msgstr "Un missatge no pot ser una resposta a si mateix" msgid "You cannot change the type of a project's tab" msgstr "No pots canviar el tipus d'una pestanya d'un projecte" -#: apps/projects/models.py:152 +#: apps/projects/models.py:153 msgid "title" msgstr "títol" -#: apps/projects/models.py:162 +#: apps/projects/models.py:163 msgid "main goal" msgstr "objectiu principal" -#: apps/projects/models.py:175 +#: apps/projects/models.py:176 msgid "life status" msgstr "estat de vida" -#: apps/projects/models.py:186 +#: apps/projects/models.py:187 msgid "categories" msgstr "categories" -#: apps/projects/models.py:203 +#: apps/projects/models.py:204 msgid "sustainable development goals" msgstr "objectius de desenvolupament sostenible" diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index 20df3307..38ee0d2a 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-05-27 10:40+0200\n" +"POT-Creation-Date: 2026-06-18 15:40+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -112,7 +112,7 @@ msgstr "Sie können diese Rolle keinem Benutzer zuweisen" msgid "You cannot assign this role to a user : {role}" msgstr "Sie können diese Rolle keinem Benutzer zuweisen: {role}" -#: apps/accounts/models.py:161 apps/projects/models.py:169 +#: apps/accounts/models.py:168 apps/projects/models.py:170 msgid "visibility" msgstr "Sichtbarkeit" @@ -1586,23 +1586,23 @@ msgstr "Eine Nachricht kann keine Antwort auf sich selbst sein" msgid "You cannot change the type of a project's tab" msgstr "Sie können den Typ eines Projekt-Tabs nicht ändern" -#: apps/projects/models.py:152 +#: apps/projects/models.py:153 msgid "title" msgstr "Titel" -#: apps/projects/models.py:162 +#: apps/projects/models.py:163 msgid "main goal" msgstr "Hauptziel" -#: apps/projects/models.py:175 +#: apps/projects/models.py:176 msgid "life status" msgstr "Lebensstatus" -#: apps/projects/models.py:186 +#: apps/projects/models.py:187 msgid "categories" msgstr "Kategorien" -#: apps/projects/models.py:203 +#: apps/projects/models.py:204 msgid "sustainable development goals" msgstr "Ziele für nachhaltige Entwicklung" diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index c9c65787..aa2f87c1 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-05-27 10:40+0200\n" +"POT-Creation-Date: 2026-06-18 15:40+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -108,7 +108,7 @@ msgstr "" msgid "You cannot assign this role to a user : {role}" msgstr "" -#: apps/accounts/models.py:161 apps/projects/models.py:169 +#: apps/accounts/models.py:168 apps/projects/models.py:170 msgid "visibility" msgstr "" @@ -1225,23 +1225,23 @@ msgstr "" msgid "You cannot change the type of a project's tab" msgstr "" -#: apps/projects/models.py:152 +#: apps/projects/models.py:153 msgid "title" msgstr "" -#: apps/projects/models.py:162 +#: apps/projects/models.py:163 msgid "main goal" msgstr "" -#: apps/projects/models.py:175 +#: apps/projects/models.py:176 msgid "life status" msgstr "" -#: apps/projects/models.py:186 +#: apps/projects/models.py:187 msgid "categories" msgstr "" -#: apps/projects/models.py:203 +#: apps/projects/models.py:204 msgid "sustainable development goals" msgstr "" diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po index 14e9dfbb..ef7bcfc2 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-05-27 10:40+0200\n" +"POT-Creation-Date: 2026-06-18 15:40+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -110,7 +110,7 @@ msgstr "No puedes asignar este rol a un usuario" msgid "You cannot assign this role to a user : {role}" msgstr "No puedes asignar este rol a un usuario: {role}" -#: apps/accounts/models.py:161 apps/projects/models.py:169 +#: apps/accounts/models.py:168 apps/projects/models.py:170 msgid "visibility" msgstr "visibilidad" @@ -1567,23 +1567,23 @@ msgstr "Un mensaje no puede ser una respuesta a sí mismo" msgid "You cannot change the type of a project's tab" msgstr "No puedes cambiar el tipo de una pestaña de proyecto" -#: apps/projects/models.py:152 +#: apps/projects/models.py:153 msgid "title" msgstr "título" -#: apps/projects/models.py:162 +#: apps/projects/models.py:163 msgid "main goal" msgstr "objetivo principal" -#: apps/projects/models.py:175 +#: apps/projects/models.py:176 msgid "life status" msgstr "estado de vida" -#: apps/projects/models.py:186 +#: apps/projects/models.py:187 msgid "categories" msgstr "categorías" -#: apps/projects/models.py:203 +#: apps/projects/models.py:204 msgid "sustainable development goals" msgstr "objetivos de desarrollo sostenible" diff --git a/locale/et/LC_MESSAGES/django.po b/locale/et/LC_MESSAGES/django.po index 6dac1e7d..eb14ef91 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-05-27 10:40+0200\n" +"POT-Creation-Date: 2026-06-18 15:40+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -110,7 +110,7 @@ msgstr "Sa ei saa seda rolli kasutajale määrata" msgid "You cannot assign this role to a user : {role}" msgstr "Sa ei saa seda rolli kasutajale määrata: {role}" -#: apps/accounts/models.py:161 apps/projects/models.py:169 +#: apps/accounts/models.py:168 apps/projects/models.py:170 msgid "visibility" msgstr "nähtavus" @@ -1546,23 +1546,23 @@ msgstr "Sõnum ei saa olla vastus iseendale" msgid "You cannot change the type of a project's tab" msgstr "Sa ei saa muuta projekti vahelehe tüüpi" -#: apps/projects/models.py:152 +#: apps/projects/models.py:153 msgid "title" msgstr "pealkiri" -#: apps/projects/models.py:162 +#: apps/projects/models.py:163 msgid "main goal" msgstr "peamine eesmärk" -#: apps/projects/models.py:175 +#: apps/projects/models.py:176 msgid "life status" msgstr "elutsükli olek" -#: apps/projects/models.py:186 +#: apps/projects/models.py:187 msgid "categories" msgstr "kategooriad" -#: apps/projects/models.py:203 +#: apps/projects/models.py:204 msgid "sustainable development goals" msgstr "säästva arengu eesmärgid" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index deb0484c..c890846b 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-05-27 10:40+0200\n" +"POT-Creation-Date: 2026-06-18 15:40+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -112,7 +112,7 @@ msgstr "Vous ne pouvez pas assigner ce rôle à un·e utilisateur·ice" msgid "You cannot assign this role to a user : {role}" msgstr "Vous ne pouvez pas assigner ce rôle à un·e utilisateur·ice : {role}" -#: apps/accounts/models.py:161 apps/projects/models.py:169 +#: apps/accounts/models.py:168 apps/projects/models.py:170 msgid "visibility" msgstr "visibilité" @@ -1563,23 +1563,23 @@ msgstr "Un message ne peut pas être une réponse à lui-même" msgid "You cannot change the type of a project's tab" msgstr "Vous ne pouvez pas changer le type d'un onglet de projet" -#: apps/projects/models.py:152 +#: apps/projects/models.py:153 msgid "title" msgstr "titre" -#: apps/projects/models.py:162 +#: apps/projects/models.py:163 msgid "main goal" msgstr "objectif principal" -#: apps/projects/models.py:175 +#: apps/projects/models.py:176 msgid "life status" msgstr "état d'avancement" -#: apps/projects/models.py:186 +#: apps/projects/models.py:187 msgid "categories" msgstr "catégories" -#: apps/projects/models.py:203 +#: apps/projects/models.py:204 msgid "sustainable development goals" msgstr "objectifs de développement durable" diff --git a/locale/nl/LC_MESSAGES/django.po b/locale/nl/LC_MESSAGES/django.po index c276e329..bc48d34a 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-05-27 10:40+0200\n" +"POT-Creation-Date: 2026-06-18 15:40+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -112,7 +112,7 @@ msgstr "Je kunt deze rol niet toewijzen aan een gebruiker" msgid "You cannot assign this role to a user : {role}" msgstr "Je kunt deze rol niet toewijzen aan een gebruiker: {role}" -#: apps/accounts/models.py:161 apps/projects/models.py:169 +#: apps/accounts/models.py:168 apps/projects/models.py:170 msgid "visibility" msgstr "zichtbaarheid" @@ -1578,23 +1578,23 @@ msgstr "Een bericht kan geen antwoord op zichzelf zijn" msgid "You cannot change the type of a project's tab" msgstr "Je kunt het type van een projecttabblad niet wijzigen" -#: apps/projects/models.py:152 +#: apps/projects/models.py:153 msgid "title" msgstr "titel" -#: apps/projects/models.py:162 +#: apps/projects/models.py:163 msgid "main goal" msgstr "hoofddoel" -#: apps/projects/models.py:175 +#: apps/projects/models.py:176 msgid "life status" msgstr "levensstatus" -#: apps/projects/models.py:186 +#: apps/projects/models.py:187 msgid "categories" msgstr "categorieën" -#: apps/projects/models.py:203 +#: apps/projects/models.py:204 msgid "sustainable development goals" msgstr "duurzame ontwikkelingsdoelen" diff --git a/services/translator/serializers.py b/services/translator/serializers.py index dfb31535..67d38fd5 100644 --- a/services/translator/serializers.py +++ b/services/translator/serializers.py @@ -1,3 +1,5 @@ +import copy + from django.conf import settings from modeltranslation.manager import get_translatable_fields_for_model from rest_framework import serializers @@ -6,6 +8,31 @@ from services.translator.mixins import HasAutoTranslatedFields +def prefix_field_langs(field: str) -> list[str]: + return [f"{field}_{lang}" for lang in settings.REQUIRED_LANGUAGES] + + +def prefix_fields_langs(fields: list[str]) -> list[list[str]]: + final = [] + for field in fields: + final.extend(prefix_field_langs(field)) + return final + + +def generate_translated_fields(fields_names: tuple[str]): + def _wraps(cls: serializers.BaseSerializer) -> serializers.BaseSerializer: + + # generates all fields + for field in fields_names: + for field_name in prefix_field_langs(field): + duplicate = copy.deepcopy(cls._declared_fields[field]) + cls._declared_fields[field_name] = duplicate + + return cls + + return _wraps + + def auto_translated(cls: serializers.ModelSerializer) -> serializers.ModelSerializer: """Automatically include translations fields for models with `HasAutoTranslatedFields` mixin. @@ -31,9 +58,10 @@ def auto_translated(cls: serializers.ModelSerializer) -> serializers.ModelSerial 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) + fields_to_add.extend(prefix_field_langs(field)) # set all fields in read_only (use set to avoid duplicated refered) read_only_fields = getattr(cls.Meta, "read_only_fields", []) @@ -73,10 +101,8 @@ def external_auto_translated( 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) + fields_to_add = prefix_fields_langs(fields_available) # set all fields in fields fields = getattr(cls.Meta, "fields", None)