From 79b0d63465f03fe66b4e3aaa57701d3c1ca69ce9 Mon Sep 17 00:00:00 2001 From: rgermain Date: Thu, 26 Mar 2026 17:13:43 +0100 Subject: [PATCH 1/6] pre optimize maps --- apps/accounts/models.py | 9 ++++++++- apps/commons/mixins.py | 5 +++++ apps/projects/models.py | 7 ++++++- apps/projects/serializers.py | 8 ++++++++ apps/projects/utils.py | 26 ++++++++++++++++++++++++++ apps/projects/views.py | 18 ++++++++---------- services/translator/serializers.py | 17 +++++++++++++++++ 7 files changed, 78 insertions(+), 12 deletions(-) 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/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/projects/models.py b/apps/projects/models.py index 7df5c813..97c824d0 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 @@ -894,7 +895,7 @@ class Meta: # TODO(remi): rename to ProjectLocation ? -class Location(ProjectRelated, AbstractLocation): +class Location(ProjectRelated, HasRelatedLocationContent, AbstractLocation): """A project location on Earth. Attributes @@ -909,6 +910,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..2b3e70c5 100644 --- a/apps/projects/serializers.py +++ b/apps/projects/serializers.py @@ -989,3 +989,11 @@ def get_string_images_kwargs( "project_id": instance.tab.project.id, "tab_id": instance.tab.id, } + + +class GeneralLocationSerializer(serializers.Serializer): + content_id = serializers.CharField() + content_type = serializers.CharField() + lat = serializers.FloatField() + lng = serializers.FloatField() + type = serializers.CharField() diff --git a/apps/projects/utils.py b/apps/projects/utils.py index 367e62a6..dd961d3b 100644 --- a/apps/projects/utils.py +++ b/apps/projects/utils.py @@ -1,5 +1,7 @@ from typing import Any, TypeVar +from django.db.models import CharField, F, QuerySet, Value +from django.db.models.functions import Cast from rest_framework import serializers from rest_framework.utils import model_meta @@ -61,3 +63,27 @@ 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 + + for queryset in querysets: + model = queryset.model + content = model.get_related_content() + qs = queryset.annotate( + content_id=Cast(f"{content}_id", output_field=CharField()), + content_type=Value(content), + ).values( + "lat", + "lng", + "type", + "content_id", + "content_type", + ) + + 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 4e7a5a5d..e844b0b7 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -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 @@ -50,6 +49,7 @@ LinkedProjectPermissionDeniedError, OrganizationsParameterMissing, ) +from apps.projects.utils import annotate_queryset_location from services.mistral.models import ProjectEmbedding from .filters import ProjectFilter @@ -66,6 +66,7 @@ from .permissions import HasProjectPermission, ProjectIsNotLocked from .serializers import ( BlogEntrySerializer, + GeneralLocationSerializer, GoalSerializer, LinkedProjectSerializer, LocationSerializer, @@ -983,19 +984,16 @@ def list(self, request, *args, **kwargs): organizations_code = get_below_hierarchy_codes((self.organization.code,)) organizations = 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) + qs_project = request.user.get_project_related_queryset(Location.objects).filter( + project__organizations__in=organizations ) qs_group = request.user.get_people_group_related_queryset( PeopleGroupLocation.objects.filter( people_group__organization__in=organizations ) - ).select_related("people_group") + ) + + qs = annotate_queryset_location(qs_project, qs_group) - data = { - "groups": PeopleGroupLocationSuperLightSerializer(qs_group, many=True).data, - "projects": LocationSerializer(qs_project, many=True).data, - } + data = GeneralLocationSerializer(list(qs), many=True).data return Response(data, status=status.HTTP_200_OK) diff --git a/services/translator/serializers.py b/services/translator/serializers.py index dfb31535..4c7bac5d 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,21 @@ from services.translator.mixins import HasAutoTranslatedFields +def generate_translated_fields(fields_names: tuple[str]): + def _wraps(cls: serializers.BaseSerializer) -> serializers.BaseSerializer: + + # generates all fields + for field in fields_names: + for lang in settings.REQUIRED_LANGUAGES: + field_name = f"{field}_{lang}" + 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. From 6dae14d68dfa207bca8dcb9e7f0f7031800d7bf0 Mon Sep 17 00:00:00 2001 From: rgermain Date: Fri, 3 Apr 2026 13:16:05 +0200 Subject: [PATCH 2/6] queryserializer mixins --- apps/accounts/views.py | 15 +++++-- apps/commons/views.py | 28 +++++++++++- apps/newsfeed/views.py | 10 +++-- apps/projects/serializers.py | 8 +++- apps/projects/utils.py | 22 +++++---- apps/projects/views.py | 72 +++++++++++++++--------------- locale/ca/LC_MESSAGES/django.po | 14 +++--- locale/de/LC_MESSAGES/django.po | 14 +++--- locale/en/LC_MESSAGES/django.po | 14 +++--- locale/es/LC_MESSAGES/django.po | 14 +++--- locale/et/LC_MESSAGES/django.po | 14 +++--- locale/fr/LC_MESSAGES/django.po | 14 +++--- locale/nl/LC_MESSAGES/django.po | 14 +++--- services/translator/serializers.py | 21 ++++++--- 14 files changed, 166 insertions(+), 108 deletions(-) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 48d9570d..02e83c67 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 @@ -89,6 +90,7 @@ PeopleGroupRemoveFeaturedProjectsSerializer, PeopleGroupRemoveTeamMembersSerializer, PeopleGroupSerializer, + PeopleGroupSuperLightSerializer, PrivacySettingsSerializer, UserAdminListSerializer, UserLighterSerializer, @@ -518,7 +520,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 @@ -530,6 +534,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") @@ -556,9 +564,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/views.py b/apps/commons/views.py index b441c0d3..aa7f1bd8 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 @@ -164,3 +165,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/views.py b/apps/newsfeed/views.py index ddd4a945..ac761e2d 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", "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"] 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/serializers.py b/apps/projects/serializers.py index 2b3e70c5..cb12bfb9 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, @@ -991,7 +994,10 @@ def get_string_images_kwargs( } +@generate_translated_fields(("title", "description")) class GeneralLocationSerializer(serializers.Serializer): + title = serializers.CharField() + description = serializers.CharField() content_id = serializers.CharField() content_type = serializers.CharField() lat = serializers.FloatField() diff --git a/apps/projects/utils.py b/apps/projects/utils.py index dd961d3b..f648de9b 100644 --- a/apps/projects/utils.py +++ b/apps/projects/utils.py @@ -1,11 +1,12 @@ from typing import Any, TypeVar -from django.db.models import CharField, F, QuerySet, Value +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 @@ -69,6 +70,17 @@ def annotate_queryset_location(*querysets: QuerySet) -> QuerySet: """annoate queryset for lazy load linked elements""" all_qs: QuerySet = None + fields = ( + "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 @@ -76,13 +88,7 @@ def annotate_queryset_location(*querysets: QuerySet) -> QuerySet: qs = queryset.annotate( content_id=Cast(f"{content}_id", output_field=CharField()), content_type=Value(content), - ).values( - "lat", - "lng", - "type", - "content_id", - "content_type", - ) + ).values(*fields) all_qs = qs if all_qs is None else all_qs.union(qs) diff --git a/apps/projects/views.py b/apps/projects/views.py index 3cee405e..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 @@ -29,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, @@ -82,6 +79,7 @@ ProjectRemoveLinkedProjectSerializer, ProjectRemoveTeamMembersSerializer, ProjectSerializer, + ProjectSuperLightSerializer, ProjectTabItemSerializer, ProjectTabSerializer, ProjectVersionListSerializer, @@ -89,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 @@ -102,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") @@ -135,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.""" @@ -176,15 +172,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) @@ -985,28 +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).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 ) ) - qs_news = request.user.get_news_related_queryset( - NewsLocation.objects.filter(news__organization__in=organizations) + + def get_queryset_news(self) -> QuerySet[NewsLocation]: + return self.request.user.get_news_related_queryset( + NewsLocation.objects.filter(news__organization__in=self.organizations) ) - qs_event = request.user.get_event_related_queryset( - EventLocation.objects.filter(event__organization__in=organizations) + def get_queryset_event(self) -> QuerySet[EventLocation]: + return self.request.user.get_event_related_queryset( + EventLocation.objects.filter(event__organization__in=self.organizations) ) - qs = annotate_queryset_location(qs_project, qs_group, qs_news, qs_event) + 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 6fd7fa60..90efd57f 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-24 20:34+0100\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 8f9d6056..fa79746a 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-24 20:34+0100\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 9911a0af..9a4caec8 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-24 20:34+0100\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 e129a5d9..8ed1e84a 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-24 20:34+0100\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 914a2f62..b3095bef 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-24 20:34+0100\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 a15aa7f7..712c18d6 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-24 20:34+0100\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 8311d449..b8aecabf 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-24 20:34+0100\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 4c7bac5d..67d38fd5 100644 --- a/services/translator/serializers.py +++ b/services/translator/serializers.py @@ -8,13 +8,23 @@ 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 lang in settings.REQUIRED_LANGUAGES: - field_name = f"{field}_{lang}" + for field_name in prefix_field_langs(field): duplicate = copy.deepcopy(cls._declared_fields[field]) cls._declared_fields[field_name] = duplicate @@ -48,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", []) @@ -90,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) From 2b29cdffe2d7084ed4fbb9f77cc99a0bc3a39cdb Mon Sep 17 00:00:00 2001 From: rgermain Date: Tue, 7 Apr 2026 17:47:31 +0200 Subject: [PATCH 3/6] test: fix general locations --- .../tests/views/test_read_location.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/apps/projects/tests/views/test_read_location.py b/apps/projects/tests/views/test_read_location.py index 00a17f36..c9e3c637 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["content_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"]}, - {a.id for a in [self.locations_group[a] for a in retrieved_locations]}, + { + location["content_id"] + for location in locations + if location["content_type"] == "people_group" + }, + {a.id for a in [self.locations[a] for a in retrieved_locations]}, ) From b856c000be5c0fa12e7128c779f3eeb477646796 Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 8 Apr 2026 14:01:24 +0200 Subject: [PATCH 4/6] test: fix id content_type --- apps/projects/tests/views/test_read_location.py | 4 ++-- apps/projects/utils.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/projects/tests/views/test_read_location.py b/apps/projects/tests/views/test_read_location.py index c9e3c637..875e8a76 100644 --- a/apps/projects/tests/views/test_read_location.py +++ b/apps/projects/tests/views/test_read_location.py @@ -116,7 +116,7 @@ def test_list_project_location(self, role, retrieved_locations): # projects (from organization, not organization_other) self.assertSetEqual( { - location["content_id"] + location["id"] for location in locations if location["content_type"] == "project" }, @@ -149,7 +149,7 @@ def test_list_group_location(self, role, retrieved_locations): # projects (from organization, not organization_other) self.assertSetEqual( { - location["content_id"] + location["id"] for location in locations if location["content_type"] == "people_group" }, diff --git a/apps/projects/utils.py b/apps/projects/utils.py index f648de9b..af64556b 100644 --- a/apps/projects/utils.py +++ b/apps/projects/utils.py @@ -71,6 +71,7 @@ def annotate_queryset_location(*querysets: QuerySet) -> QuerySet: all_qs: QuerySet = None fields = ( + "id", "lat", "lng", "type", @@ -86,6 +87,7 @@ def annotate_queryset_location(*querysets: QuerySet) -> QuerySet: 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) From 94fae1e034bc5490a156bfa48d4e332398d46172 Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 8 Apr 2026 15:23:14 +0200 Subject: [PATCH 5/6] fix: missing id serializers --- apps/projects/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/projects/serializers.py b/apps/projects/serializers.py index cb12bfb9..bf48edba 100644 --- a/apps/projects/serializers.py +++ b/apps/projects/serializers.py @@ -996,6 +996,7 @@ def get_string_images_kwargs( @generate_translated_fields(("title", "description")) class GeneralLocationSerializer(serializers.Serializer): + id = serializers.IntegerField() title = serializers.CharField() description = serializers.CharField() content_id = serializers.CharField() From a6451e277390e848516c937b9a699282c62d8e0f Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 8 Apr 2026 16:19:33 +0200 Subject: [PATCH 6/6] test: fix locations groups --- apps/projects/tests/views/test_read_location.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/projects/tests/views/test_read_location.py b/apps/projects/tests/views/test_read_location.py index 875e8a76..992f6779 100644 --- a/apps/projects/tests/views/test_read_location.py +++ b/apps/projects/tests/views/test_read_location.py @@ -153,5 +153,5 @@ def test_list_group_location(self, role, retrieved_locations): for location in locations if location["content_type"] == "people_group" }, - {a.id for a in [self.locations[a] for a in retrieved_locations]}, + {a.id for a in [self.locations_group[a] for a in retrieved_locations]}, )