diff --git a/apps/accounts/models.py b/apps/accounts/models.py index 5d54fba7..744d348e 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 fbcd19de..453d5a90 100644 --- a/apps/commons/mixins.py +++ b/apps/commons/mixins.py @@ -476,3 +476,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 40dfe801..f5e2df6b 100644 --- a/apps/commons/views.py +++ b/apps/commons/views.py @@ -1,5 +1,6 @@ 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 @@ -165,3 +166,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 5ed3dc80..c915d640 100644 --- a/apps/newsfeed/views.py +++ b/apps/newsfeed/views.py @@ -13,7 +13,7 @@ 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 +from apps.commons.views import ListViewSet, QuerySerializersMixin from apps.files.models import Image from apps.files.views import ImageStorageView from apps.organizations.permissions import HasOrganizationPermission @@ -22,9 +22,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 +121,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 +130,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") @@ -321,7 +324,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 @@ -330,6 +333,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/projects/models.py b/apps/projects/models.py index dde29d26..e6668b83 100644 --- a/apps/projects/models.py +++ b/apps/projects/models.py @@ -23,6 +23,7 @@ HasMultipleIDs, HasOwner, HasPermissionsSetup, + HasRelatedLocationContent, ProjectRelated, ) from apps.commons.models import GroupData @@ -896,7 +897,7 @@ class Meta: # TODO(remi): rename to ProjectLocation ? -class Location(ProjectRelated, AbstractLocation): +class Location(ProjectRelated, HasRelatedLocationContent, AbstractLocation): """A project location on Earth. Attributes @@ -911,6 +912,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 5364ad6d..bf48edba 100644 --- a/apps/projects/serializers.py +++ b/apps/projects/serializers.py @@ -43,7 +43,10 @@ ) from apps.skills.models import Tag from apps.skills.serializers import TagRelatedField, TagSerializer -from services.translator.serializers import auto_translated +from services.translator.serializers import ( + auto_translated, + generate_translated_fields, +) from .exceptions import ( AddProjectToOrganizationPermissionError, @@ -989,3 +992,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 2ad024e7..c8da7826 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -1,5 +1,5 @@ -import enum import uuid +from functools import cached_property from django.apps import apps from django.conf import settings @@ -22,7 +22,6 @@ from apps.accounts.models import PeopleGroupLocation from apps.accounts.permissions import HasBasePermission -from apps.accounts.serializers import PeopleGroupLocationSuperLightSerializer from apps.analytics.models import Stat from apps.commons.cache import clear_cache_with_key, redis_cache_view from apps.commons.permissions import IsOwner, ReadOnly @@ -30,14 +29,11 @@ from apps.commons.views import ( MultipleIDViewsetMixin, NestedOrganizationViewMixins, + QuerySerializersMixin, ) from apps.files.models import Image from apps.files.views import ImageStorageView from apps.newsfeed.models import EventLocation, NewsLocation -from apps.newsfeed.serializers import ( - EventLocationSerializerLight, - NewsLocationSerializerLight, -) from apps.notifications.tasks import ( notify_group_as_member_added, notify_group_member_deleted, @@ -55,6 +51,7 @@ LinkedProjectPermissionDeniedError, OrganizationsParameterMissing, ) +from apps.projects.utils import annotate_queryset_location from services.mistral.models import ProjectEmbedding from .filters import ProjectFilter @@ -71,6 +68,7 @@ from .permissions import HasProjectPermission, ProjectIsNotLocked from .serializers import ( BlogEntrySerializer, + GeneralLocationSerializer, GoalSerializer, LinkedProjectSerializer, LocationSerializer, @@ -81,6 +79,7 @@ ProjectRemoveLinkedProjectSerializer, ProjectRemoveTeamMembersSerializer, ProjectSerializer, + ProjectSuperLightSerializer, ProjectTabItemSerializer, ProjectTabSerializer, ProjectVersionListSerializer, @@ -88,12 +87,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 @@ -101,6 +99,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") @@ -134,13 +136,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.""" @@ -172,21 +169,13 @@ def perform_update(self, serializer: ProjectSerializer): def perform_destroy(self, instance): if settings.ENABLE_CACHE and instance.announcements.exists(): cache.delete_many(cache.keys("announcements_list_cache*")) - super(ProjectViewSet, self).perform_destroy(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(ProjectViewSet, self).list(request, *args, **kwargs) + return super().list(request, *args, **kwargs) @extend_schema( parameters=[ @@ -602,11 +591,11 @@ def get_queryset(self): redis_cache_view("locations_list_cache", settings.CACHE_LOCATIONS_LIST_TTL) ) def list(self, request, *args, **kwargs): - return super(LocationViewSet, self).list(request, *args, **kwargs) + return super().list(request, *args, **kwargs) @method_decorator(clear_cache_with_key("locations_list_cache")) def dispatch(self, request, *args, **kwargs): - return super(LocationViewSet, self).dispatch(request, *args, **kwargs) + return super().dispatch(request, *args, **kwargs) class HistoricalProjectViewSet(MultipleIDViewsetMixin, viewsets.ReadOnlyModelViewSet): @@ -662,14 +651,14 @@ def check_linked_project_permission(self, project): def perform_create(self, serializer): project = serializer.validated_data["project"] self.check_linked_project_permission(project) - super(LinkedProjectViewSet, self).perform_create(serializer) + super().perform_create(serializer) @transaction.atomic def perform_update(self, serializer): project = serializer.validated_data.get("project") if project: self.check_linked_project_permission(project) - super(LinkedProjectViewSet, self).perform_update(serializer) + super().perform_update(serializer) @extend_schema( request=ProjectAddLinkedProjectSerializer, responses=ProjectSerializer @@ -984,34 +973,40 @@ def add_image_to_model(self, image, *args, **kwargs): class GeneralLocationView(NestedOrganizationViewMixins, viewsets.GenericViewSet): http_method_names = ["get", "list"] - def list(self, request, *args, **kwargs): + @cached_property + def organizations(self) -> QuerySet[Organization]: organizations_code = get_below_hierarchy_codes((self.organization.code,)) - organizations = Organization.objects.filter(code__in=organizations_code) + return Organization.objects.filter(code__in=organizations_code) - qs_project = ( - request.user.get_project_related_queryset(Location.objects) - .select_related("project") - .filter(project__organizations__in=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=organizations + people_group__organization__in=self.organizations ) - ).select_related("people_group") - - qs_news = request.user.get_news_related_queryset( - NewsLocation.objects.filter(news__organization__in=organizations) - ).select_related("news") - - qs_event = request.user.get_event_related_queryset( - EventLocation.objects.filter(event__organization__in=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 get_queryset_news(self) -> QuerySet[NewsLocation]: + return self.request.user.get_news_related_queryset( + NewsLocation.objects.filter(news__organization__in=self.organizations) + ) + + def get_queryset_event(self) -> QuerySet[EventLocation]: + return self.request.user.get_event_related_queryset( + EventLocation.objects.filter(event__organization__in=self.organizations) + ) + + 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/locale/ca/LC_MESSAGES/django.po b/locale/ca/LC_MESSAGES/django.po index 2971e62e..5004c4a2 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-06 09:44+0200\n" +"POT-Creation-Date: 2026-04-03 13:14+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:164 +#: apps/accounts/models.py:168 apps/projects/models.py:165 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:147 +#: apps/projects/models.py:148 msgid "title" msgstr "títol" -#: apps/projects/models.py:157 +#: apps/projects/models.py:158 msgid "main goal" msgstr "objectiu principal" -#: apps/projects/models.py:170 +#: apps/projects/models.py:171 msgid "life status" msgstr "estat de vida" -#: apps/projects/models.py:181 +#: apps/projects/models.py:182 msgid "categories" msgstr "categories" -#: apps/projects/models.py:198 +#: apps/projects/models.py:199 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 fec09a5c..02c6c282 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-06 09:44+0200\n" +"POT-Creation-Date: 2026-04-03 13:14+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:164 +#: apps/accounts/models.py:168 apps/projects/models.py:165 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:147 +#: apps/projects/models.py:148 msgid "title" msgstr "Titel" -#: apps/projects/models.py:157 +#: apps/projects/models.py:158 msgid "main goal" msgstr "Hauptziel" -#: apps/projects/models.py:170 +#: apps/projects/models.py:171 msgid "life status" msgstr "Lebensstatus" -#: apps/projects/models.py:181 +#: apps/projects/models.py:182 msgid "categories" msgstr "Kategorien" -#: apps/projects/models.py:198 +#: apps/projects/models.py:199 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 4426e98b..338b83d4 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-06 09:44+0200\n" +"POT-Creation-Date: 2026-04-03 13:14+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:164 +#: apps/accounts/models.py:168 apps/projects/models.py:165 msgid "visibility" msgstr "" @@ -1225,23 +1225,23 @@ msgstr "" msgid "You cannot change the type of a project's tab" msgstr "" -#: apps/projects/models.py:147 +#: apps/projects/models.py:148 msgid "title" msgstr "" -#: apps/projects/models.py:157 +#: apps/projects/models.py:158 msgid "main goal" msgstr "" -#: apps/projects/models.py:170 +#: apps/projects/models.py:171 msgid "life status" msgstr "" -#: apps/projects/models.py:181 +#: apps/projects/models.py:182 msgid "categories" msgstr "" -#: apps/projects/models.py:198 +#: apps/projects/models.py:199 msgid "sustainable development goals" msgstr "" diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po index 82d96659..e03bc18c 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-06 09:44+0200\n" +"POT-Creation-Date: 2026-04-03 13:14+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:164 +#: apps/accounts/models.py:168 apps/projects/models.py:165 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:147 +#: apps/projects/models.py:148 msgid "title" msgstr "título" -#: apps/projects/models.py:157 +#: apps/projects/models.py:158 msgid "main goal" msgstr "objetivo principal" -#: apps/projects/models.py:170 +#: apps/projects/models.py:171 msgid "life status" msgstr "estado de vida" -#: apps/projects/models.py:181 +#: apps/projects/models.py:182 msgid "categories" msgstr "categorías" -#: apps/projects/models.py:198 +#: apps/projects/models.py:199 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 9d680516..96744db5 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-06 09:44+0200\n" +"POT-Creation-Date: 2026-04-03 13:14+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:164 +#: apps/accounts/models.py:168 apps/projects/models.py:165 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:147 +#: apps/projects/models.py:148 msgid "title" msgstr "pealkiri" -#: apps/projects/models.py:157 +#: apps/projects/models.py:158 msgid "main goal" msgstr "peamine eesmärk" -#: apps/projects/models.py:170 +#: apps/projects/models.py:171 msgid "life status" msgstr "elutsükli olek" -#: apps/projects/models.py:181 +#: apps/projects/models.py:182 msgid "categories" msgstr "kategooriad" -#: apps/projects/models.py:198 +#: apps/projects/models.py:199 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 b2db29a8..a6a4c5a2 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-06 09:44+0200\n" +"POT-Creation-Date: 2026-04-03 13:14+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:164 +#: apps/accounts/models.py:168 apps/projects/models.py:165 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:147 +#: apps/projects/models.py:148 msgid "title" msgstr "titre" -#: apps/projects/models.py:157 +#: apps/projects/models.py:158 msgid "main goal" msgstr "objectif principal" -#: apps/projects/models.py:170 +#: apps/projects/models.py:171 msgid "life status" msgstr "état d'avancement" -#: apps/projects/models.py:181 +#: apps/projects/models.py:182 msgid "categories" msgstr "catégories" -#: apps/projects/models.py:198 +#: apps/projects/models.py:199 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 726bc9bc..51187295 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-06 09:44+0200\n" +"POT-Creation-Date: 2026-04-03 13:14+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:164 +#: apps/accounts/models.py:168 apps/projects/models.py:165 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:147 +#: apps/projects/models.py:148 msgid "title" msgstr "titel" -#: apps/projects/models.py:157 +#: apps/projects/models.py:158 msgid "main goal" msgstr "hoofddoel" -#: apps/projects/models.py:170 +#: apps/projects/models.py:171 msgid "life status" msgstr "levensstatus" -#: apps/projects/models.py:181 +#: apps/projects/models.py:182 msgid "categories" msgstr "categorieën" -#: apps/projects/models.py:198 +#: apps/projects/models.py:199 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)