From 7c0de29af2f0d1dcac60e51f454310d980f96c84 Mon Sep 17 00:00:00 2001 From: rgermain Date: Mon, 21 Sep 2026 17:36:51 +0200 Subject: [PATCH 01/11] optimize(Queryset): reduce queryset --- apps/accounts/models.py | 6 +++--- apps/accounts/serializers.py | 19 ------------------- apps/commons/fields.py | 4 ++-- 3 files changed, 5 insertions(+), 24 deletions(-) diff --git a/apps/accounts/models.py b/apps/accounts/models.py index 29907e25..644eaece 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -548,7 +548,7 @@ def get_news_queryset(self) -> QuerySet["News"]: self._news_queryset = News.objects.all() else: groups = PeopleGroup.objects.filter(groups__users=self) - organizations = self.get_related_organizations() + organizations = self.get_organizations_queryset() self._news_queryset = News.objects.filter( Q(visible_by_all=True) | Q(people_groups__in=groups) @@ -570,7 +570,7 @@ def get_instruction_queryset(self) -> QuerySet["Instruction"]: self._instruction_queryset = Instruction.objects.all() else: groups = PeopleGroup.objects.filter(groups__users=self) - organizations = self.get_related_organizations() + organizations = self.get_organizations_queryset() self._instruction_queryset = Instruction.objects.filter( Q(visible_by_all=True) | Q(people_groups__in=groups) @@ -592,7 +592,7 @@ def get_event_queryset(self) -> QuerySet["Event"]: self._event_queryset = Event.objects.all() else: groups = PeopleGroup.objects.filter(groups__users=self) - organizations = self.get_related_organizations() + organizations = self.get_organizations_queryset() self._event_queryset = Event.objects.filter( Q(visible_by_all=True) | Q(people_groups__in=groups) diff --git a/apps/accounts/serializers.py b/apps/accounts/serializers.py index 10ac138c..1aaa43d9 100644 --- a/apps/accounts/serializers.py +++ b/apps/accounts/serializers.py @@ -1,5 +1,4 @@ import uuid -from functools import cached_property from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType @@ -40,7 +39,6 @@ UserRolePermissionDeniedError, ) from .models import ( - AnonymousUser, PeopleGroup, PeopleGroupLocation, PrivacySettings, @@ -218,23 +216,6 @@ class Meta: "profile_picture_natural_ratio", ] - @cached_property - def _user_acces(self): - request = self.context.get("request") - if request: - return request.user.get_user_queryset().values_list("pk", flat=True) - return [] - - def to_representation(self, instance: ProjectUser): - force_display = self.context.get("force_display", False) - if force_display or instance.pk in self._user_acces: - return super().to_representation(instance) - - return { - **AnonymousUser.serialize(with_permissions=False), - "current_org_role": None, - } - def _validate_role( self, group: Group, diff --git a/apps/commons/fields.py b/apps/commons/fields.py index 65fc086f..a5363bc0 100644 --- a/apps/commons/fields.py +++ b/apps/commons/fields.py @@ -222,14 +222,14 @@ def _check_privacy_settings(self, value): get_superadmins_group() ): return True - settings, _ = PrivacySettings.objects.get_or_create(user=instance) + settings = instance.privacy_settings match getattr(settings, self.privacy_field): case PrivacySettings.PrivacyChoices.PUBLIC: return True case PrivacySettings.PrivacyChoices.ORGANIZATION: return instance.groups.filter( organizations__isnull=False, - organizations__in=request.user.get_related_organizations(), + organizations__in=request.user.get_organizations_queryset(), ).exists() case PrivacySettings.PrivacyChoices.HIDE: if not request.user.is_authenticated or not isinstance( From 687980befcc3c082c701d3b665d5678006b2bfd1 Mon Sep 17 00:00:00 2001 From: rgermain Date: Tue, 22 Sep 2026 17:03:02 +0200 Subject: [PATCH 02/11] optimize privacy --- apps/accounts/serializers.py | 2 + apps/accounts/views.py | 10 ++-- apps/commons/fields.py | 95 +++------------------------------ apps/commons/serializers.py | 75 +++++++++++++++++++++++++- apps/invitations/serializers.py | 3 +- apps/projects/views.py | 6 ++- apps/search/views.py | 12 ++--- services/crisalid/models.py | 7 ++- services/mistral/views.py | 33 ++++++------ 9 files changed, 122 insertions(+), 121 deletions(-) diff --git a/apps/accounts/serializers.py b/apps/accounts/serializers.py index 1aaa43d9..16ec64ce 100644 --- a/apps/accounts/serializers.py +++ b/apps/accounts/serializers.py @@ -17,6 +17,7 @@ from apps.commons.models import GroupData from apps.commons.serializers import ( BaseLocationSerializer, + PrivacySerializer, StringsImagesSerializer, ) from apps.files.models import Image @@ -67,6 +68,7 @@ class Meta: @auto_translated class UserSerializer( ModulesSerializers, + PrivacySerializer, StringsImagesSerializer, serializers.ModelSerializer, ): diff --git a/apps/accounts/views.py b/apps/accounts/views.py index f01c6e95..9ed72cad 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -206,7 +206,7 @@ def get_queryset(self): if self.action == "admin_list": queryset = self.annotate_keycloak_email_verified(queryset) - return queryset.select_related("researcher") + return queryset.select_related("researcher", "privacy_settings") def get_object(self): """ @@ -257,7 +257,7 @@ def get_serializer_context(self): permission_classes=[HasBasePermission("get_user_by_email", "accounts")], ) def get_by_email(self, request, *args, **kwargs): - queryset = ProjectUser.objects.all() + queryset = ProjectUser.objects.all().select_related("privacy_settings") if self.organization is not None: queryset = self.annotate_organization_role(queryset, self.organization) user = queryset.filter( @@ -746,7 +746,11 @@ def remove_member(self, request, *args, **kwargs): def member(self, request, *args, **kwargs): group = self.get_object() - queryset = group.modules_by_user(request.user).members() + queryset = ( + group.modules_by_user(request.user) + .members() + .select_related("privacy_settings") + ) page = self.paginate_queryset(queryset) if page is not None: diff --git a/apps/commons/fields.py b/apps/commons/fields.py index a5363bc0..07a48d64 100644 --- a/apps/commons/fields.py +++ b/apps/commons/fields.py @@ -1,8 +1,5 @@ import inspect -from contextlib import suppress -from django.contrib.auth.models import Group -from django.db.models import Q, QuerySet from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ from drf_spectacular.types import OpenApiTypes @@ -12,9 +9,7 @@ from rest_framework.serializers import BaseSerializer from apps.accounts.models import PrivacySettings, ProjectUser -from apps.accounts.utils import get_superadmins_group -from apps.commons.models import GroupData -from services.crisalid.models import Researcher +from apps.commons.serializers import PrivacySerializer @extend_schema_field(OpenApiTypes.UUID) @@ -161,6 +156,8 @@ def to_internal_value(self, data): class PrivacySettingFieldMixin: + parent: PrivacySerializer + def __init__(self, **kwargs): self.privacy_field = kwargs.pop("privacy_field", "") self.default_value = kwargs.pop("default_value", None) @@ -169,90 +166,10 @@ def __init__(self, **kwargs): ] super().__init__(**kwargs) - def _get_user(self, value): - if isinstance(value, ProjectUser): - return value - if isinstance(value, Researcher): - return value.user - - user_data = getattr(self.parent, "instance", None) or getattr( - self.parent, "queryset", None - ) - if ( - user_data - and isinstance(user_data, QuerySet) - and user_data.model == ProjectUser - and user_data.count() == 1 - ): - return user_data.get() - if ( - user_data - and isinstance(user_data, QuerySet) - and user_data.model == ProjectUser - and self.source_attrs - ): - try: - return user_data.filter(**{self.source_attrs[0]: value}).first() - except TypeError: # filter raises a TypeError if queryset has been sliced - user_data = list(user_data) - if user_data and isinstance(user_data, ProjectUser): - return user_data - if user_data and isinstance(user_data, list) and len(user_data) == 1: - return user_data[0] - if user_data and isinstance(user_data, list) and self.source_attrs: - return [ - user - for user in user_data - if getattr(user, self.source_attrs[0]) == value - ][0] - if self.source_attrs: - with suppress( - ProjectUser.MultipleObjectsReturned, ProjectUser.DoesNotExist - ): - return ProjectUser.objects.get(**{self.source_attrs[0]: value}) - return None - - def _check_privacy_settings(self, value): - instance = self._get_user(value) - assert isinstance(instance, ProjectUser) - request = self.context.get("request") - assert request is not None - - if instance == request.user or request.user.groups.contains( - get_superadmins_group() - ): - return True - settings = instance.privacy_settings - match getattr(settings, self.privacy_field): - case PrivacySettings.PrivacyChoices.PUBLIC: - return True - case PrivacySettings.PrivacyChoices.ORGANIZATION: - return instance.groups.filter( - organizations__isnull=False, - organizations__in=request.user.get_organizations_queryset(), - ).exists() - case PrivacySettings.PrivacyChoices.HIDE: - if not request.user.is_authenticated or not isinstance( - request.user, ProjectUser - ): - return False - return Group.objects.filter( - Q( - organizations__isnull=False, - organizations__in=instance.get_organizations_queryset(), - users=request.user, - ) - & ( - Q(data__role=GroupData.Role.ADMINS) - | Q(data__role=GroupData.Role.FACILITATORS) - ) - ).exists() - return False - def to_representation(self, value): - if self._check_privacy_settings(value): - return super().to_representation(value) - return self.default_value + if self.parent._field_is_private(self.privacy_field): + return self.default_value + return super().to_representation(value) class PrivacySettingProtectedCharField(PrivacySettingFieldMixin, serializers.CharField): diff --git a/apps/commons/serializers.py b/apps/commons/serializers.py index a76dc521..ebd84bfb 100644 --- a/apps/commons/serializers.py +++ b/apps/commons/serializers.py @@ -1,12 +1,16 @@ from collections.abc import Collection -from typing import Any +from functools import cached_property +from typing import Any, Optional +from django.contrib.auth.models import Group from django.db.models import Model, Q from django.utils.translation import gettext_lazy as _ from rest_framework import mixins, serializers, viewsets from rest_framework.settings import import_from_string -from apps.accounts.models import ProjectUser +from apps.accounts.models import PrivacySettings, ProjectUser +from apps.commons.mixins import HasOwner +from apps.commons.models import GroupData from apps.commons.utils import process_text, remove_images_text from apps.files.models import Image from apps.organizations.models import Organization @@ -216,3 +220,70 @@ def valiate_lat(self, value): def valiate_lng(self, value): return self._check_gis(super().validate_lng(value)) + + +class PrivacySerializer: + instance: ProjectUser + + @cached_property + def _get_user(self): + if isinstance(self.instance, ProjectUser): + return self.instance + if isinstance(self.instance, HasOwner): + return self.instance.get_owner() + return None + + @cached_property + def _privacy_settings(self) -> tuple[Optional[PrivacySettings], bool, bool]: + + instance = self._get_user + if instance is None: + return None, False, False + + request = self.context.get("request") + assert request is not None + user: ProjectUser = request.user + + settings = instance.privacy_settings + + if user.is_anonymous: + is_in_org = is_org_admin = False + elif user.pk == instance.pk or user.is_superuser: + return settings, True, True + else: + is_in_org = instance.groups.filter( + organizations__isnull=False, + organizations__in=request.user.get_organizations_queryset(), + ).exists() + + is_org_admin = Group.objects.filter( + Q( + organizations__isnull=False, + organizations__in=instance.get_organizations_queryset(), + users=request.user, + ) + & ( + Q(data__role=GroupData.Role.ADMINS) + | Q(data__role=GroupData.Role.FACILITATORS) + ) + ).exists() + + return settings, is_in_org, is_org_admin + + def _field_is_private(self, field: str) -> bool: + """check if field from privacysettings is private from user""" + + privacy_settings, is_in_org, is_org_admin = self._privacy_settings + + # not privacy_settings, return all privayc field + if privacy_settings is None: + return True + + match getattr(privacy_settings, field): + case PrivacySettings.PrivacyChoices.PUBLIC: + return False + case PrivacySettings.PrivacyChoices.ORGANIZATION: + return not is_in_org + case PrivacySettings.PrivacyChoices.HIDE: + return not is_org_admin + return True diff --git a/apps/invitations/serializers.py b/apps/invitations/serializers.py index 3d6a338a..537a8e56 100644 --- a/apps/invitations/serializers.py +++ b/apps/invitations/serializers.py @@ -93,7 +93,8 @@ class AccessRequestSerializer( slug_field="code", queryset=Organization.objects.all() ) user = UserMultipleIdRelatedField( - queryset=ProjectUser.objects.all(), allow_null=True + queryset=ProjectUser.objects.all().select_related("privacy_settings"), + allow_null=True, ) class Meta: diff --git a/apps/projects/views.py b/apps/projects/views.py index 00347fa2..0a2f442c 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -365,7 +365,11 @@ class ProjectMemberViewSet( ] def get_queryset(self) -> QuerySet[ProjectUser]: - return self.project.modules_by_user(self.request.user).members() + return ( + self.project.modules_by_user(self.request.user) + .members() + .select_related("privacy_settings") + ) @extend_schema(request=ProjectAddTeamMembersSerializer, responses=ProjectSerializer) @action( diff --git a/apps/search/views.py b/apps/search/views.py index a57b379e..9883cdf6 100644 --- a/apps/search/views.py +++ b/apps/search/views.py @@ -38,7 +38,9 @@ def get_queryset(self, order: bool = True) -> QuerySet[SearchObject]: ) | (Q(type=SearchObject.SearchObjectType.USER) & Q(user__in=users)) ) - .select_related("user", "project__header_image", "people_group") + .select_related( + "user__privacy_settings", "project__header_image", "people_group" + ) .prefetch_related("people_group__organization", "project__categories") ) if order: @@ -86,10 +88,9 @@ def get_queryset(self, order: bool = True) -> QuerySet[SearchObject]: @action(detail=False, methods=["GET"], url_path="(?P.+)") def search(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) - search_objects = list(queryset) # generate ids for opensearch - search_objects_ids = [sobj.id for sobj in search_objects] + search_objects_ids = queryset.values_list("id", flat=True) query = self.kwargs.get("search", "") indices = [ @@ -138,9 +139,8 @@ def search(self, request, *args, **kwargs): search_objects_ids = [hit.search_object_id for hit in response.hits] # remove search id not hits in opensearch - filtered_search_object = [ - obj for obj in search_objects if obj.id in search_objects_ids - ] + filtered_search_object = queryset.filter(id__in=search_objects_ids) + # sort filtered_search_object by hits index ordered_search_objs = sorted( filtered_search_object, diff --git a/services/crisalid/models.py b/services/crisalid/models.py index c74dc39e..cf0d6827 100644 --- a/services/crisalid/models.py +++ b/services/crisalid/models.py @@ -6,7 +6,7 @@ from django.db.models.functions import Lower from django.utils.translation import gettext_lazy as _ -from apps.commons.mixins import HasEmbedding, OrganizationRelated +from apps.commons.mixins import HasEmbedding, HasOwner, OrganizationRelated from apps.organizations.models import Organization from services.crisalid.relators import RolesChoices from services.translator.mixins import HasAutoTranslatedFields @@ -119,7 +119,7 @@ def __str__(self): return f"{self.harvester} :: {self.value}" -class Researcher(CrisalidDataModel): +class Researcher(HasOwner, CrisalidDataModel): """Link to a crisalid""" PRIVACY_HARVESTER = ( @@ -152,6 +152,9 @@ def __str__(self): def display_name(self): return f"{self.given_name.capitalize()} {self.family_name.capitalize()}" + def get_owner(self): + return self.user + class DocumentContributor(models.Model): roles = ChoiceArrayField( diff --git a/services/mistral/views.py b/services/mistral/views.py index d2239b4f..d3b55b10 100644 --- a/services/mistral/views.py +++ b/services/mistral/views.py @@ -11,7 +11,7 @@ from apps.accounts.serializers import UserLightSerializer from apps.commons.cache import redis_cache_viewset_method from apps.commons.permissions import ReadOnly -from apps.commons.views import MultipleIDViewsetMixin +from apps.commons.views import MultipleIDViewsetMixin, NestedOrganizationViewMixins from apps.organizations.utils import get_below_hierarchy_codes from apps.projects.models import Project from apps.projects.serializers import ProjectLightSerializer @@ -190,7 +190,9 @@ def random_user_recommendations(self, request, *args, **kwargs): return Response(serializer.data) -class ProjectRecommendationsViewset(RecommendationsViewset): +class ProjectRecommendationsViewset( + NestedOrganizationViewMixins, RecommendationsViewset +): queryset = Project.objects.all() serializer_class = ProjectLightSerializer @@ -198,9 +200,7 @@ def get_queryset_for_project(self, project: Project) -> QuerySet[Project]: queryset = ( self.request.user.get_project_queryset() .filter( - organizations__code__in=get_below_hierarchy_codes( - [self.kwargs["organization_code"]] - ), + organizations__code__in=self.organizations, score__activity__gte=0.37, # 6 months of inactivity ) .exclude(id=project.id) @@ -215,12 +215,11 @@ def get_queryset_for_user(self, user: ProjectUser) -> QuerySet[Project]: queryset = ( user.get_project_queryset() .filter( - organizations__code__in=get_below_hierarchy_codes( - [self.kwargs["organization_code"]] - ), + organizations__code__in=self.organizations, score__activity__gte=0.37, # 6 months of inactivity ) .prefetch_related("categories") + .select_related("privacy_settings") ) embedding = self.get_user_embedding(user) if user.is_authenticated: @@ -230,7 +229,7 @@ def get_queryset_for_user(self, user: ProjectUser) -> QuerySet[Project]: return queryset.order_by("-score__score") -class UserRecommendationsViewset(RecommendationsViewset): +class UserRecommendationsViewset(NestedOrganizationViewMixins, RecommendationsViewset): queryset = ProjectUser.objects.all() serializer_class = UserLightSerializer @@ -238,9 +237,7 @@ def get_queryset_for_project(self, project: Project) -> QuerySet[ProjectUser]: queryset = ( self.request.user.get_user_queryset() .filter( - groups__organizations__code__in=get_below_hierarchy_codes( - [self.kwargs["organization_code"]] - ), + groups__organizations__in=self.organizations, score__activity__gte=0.1, # 49 weeks of inactivity ) .exclude(groups__projects__id=project.id) @@ -253,11 +250,13 @@ def get_queryset_for_project(self, project: Project) -> QuerySet[ProjectUser]: return queryset.none() def get_queryset_for_user(self, user: ProjectUser) -> QuerySet[ProjectUser]: - queryset = user.get_user_queryset().filter( - groups__organizations__code__in=get_below_hierarchy_codes( - [self.kwargs["organization_code"]] - ), - score__activity__gte=0.1, # 49 weeks of inactivity + queryset = ( + user.get_user_queryset() + .filter( + groups__organizations__code__in=self.organizations, + score__activity__gte=0.1, # 49 weeks of inactivity + ) + .select_related("privacy_settings") ) embedding = self.get_user_embedding(user) if user.is_authenticated: From 3b4b7aea1b7c68d8b1520668b076acae263a3f57 Mon Sep 17 00:00:00 2001 From: rgermain Date: Tue, 22 Sep 2026 17:09:12 +0200 Subject: [PATCH 03/11] fix: organizations recomendatons filters --- services/mistral/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/mistral/views.py b/services/mistral/views.py index d3b55b10..e6d21f81 100644 --- a/services/mistral/views.py +++ b/services/mistral/views.py @@ -200,7 +200,7 @@ def get_queryset_for_project(self, project: Project) -> QuerySet[Project]: queryset = ( self.request.user.get_project_queryset() .filter( - organizations__code__in=self.organizations, + organizations__in=self.organizations, score__activity__gte=0.37, # 6 months of inactivity ) .exclude(id=project.id) @@ -215,7 +215,7 @@ def get_queryset_for_user(self, user: ProjectUser) -> QuerySet[Project]: queryset = ( user.get_project_queryset() .filter( - organizations__code__in=self.organizations, + organizations__in=self.organizations, score__activity__gte=0.37, # 6 months of inactivity ) .prefetch_related("categories") @@ -253,7 +253,7 @@ def get_queryset_for_user(self, user: ProjectUser) -> QuerySet[ProjectUser]: queryset = ( user.get_user_queryset() .filter( - groups__organizations__code__in=self.organizations, + groups__organizations__in=self.organizations, score__activity__gte=0.1, # 49 weeks of inactivity ) .select_related("privacy_settings") From a7e789f31246ca7899e690a3756693c49b748690 Mon Sep 17 00:00:00 2001 From: rgermain Date: Tue, 22 Sep 2026 17:41:32 +0200 Subject: [PATCH 04/11] fix: privacy instances --- apps/commons/fields.py | 6 +++++- apps/commons/serializers.py | 29 +++++++++++++++++------------ apps/search/views.py | 2 +- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/apps/commons/fields.py b/apps/commons/fields.py index 07a48d64..1bce5b1a 100644 --- a/apps/commons/fields.py +++ b/apps/commons/fields.py @@ -166,8 +166,12 @@ def __init__(self, **kwargs): ] super().__init__(**kwargs) + def get_attribute(self, instance): + self._instance = instance + return super().get_attribute(instance) + def to_representation(self, value): - if self.parent._field_is_private(self.privacy_field): + if self.parent._field_is_private(self._instance, self.privacy_field): return self.default_value return super().to_representation(value) diff --git a/apps/commons/serializers.py b/apps/commons/serializers.py index ebd84bfb..773101ca 100644 --- a/apps/commons/serializers.py +++ b/apps/commons/serializers.py @@ -1,5 +1,5 @@ from collections.abc import Collection -from functools import cached_property +from functools import cache from typing import Any, Optional from django.contrib.auth.models import Group @@ -225,18 +225,23 @@ def valiate_lng(self, value): class PrivacySerializer: instance: ProjectUser - @cached_property - def _get_user(self): - if isinstance(self.instance, ProjectUser): - return self.instance - if isinstance(self.instance, HasOwner): - return self.instance.get_owner() + def __init__(self, *ar, **kw): + super().__init__(*ar, **kw) + self._privacy_settings = cache(self._privacy_settings) + + def _get_user(self, instance): + if isinstance(instance, ProjectUser): + return instance + if isinstance(instance, HasOwner): + return instance.get_owner() return None - @cached_property - def _privacy_settings(self) -> tuple[Optional[PrivacySettings], bool, bool]: + def _privacy_settings( + self, instance + ) -> tuple[Optional[PrivacySettings], bool, bool]: + + instance = self._get_user(instance) - instance = self._get_user if instance is None: return None, False, False @@ -270,10 +275,10 @@ def _privacy_settings(self) -> tuple[Optional[PrivacySettings], bool, bool]: return settings, is_in_org, is_org_admin - def _field_is_private(self, field: str) -> bool: + def _field_is_private(self, instance, field: str) -> bool: """check if field from privacysettings is private from user""" - privacy_settings, is_in_org, is_org_admin = self._privacy_settings + privacy_settings, is_in_org, is_org_admin = self._privacy_settings(instance) # not privacy_settings, return all privayc field if privacy_settings is None: diff --git a/apps/search/views.py b/apps/search/views.py index 9883cdf6..002bf867 100644 --- a/apps/search/views.py +++ b/apps/search/views.py @@ -90,7 +90,7 @@ def search(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) # generate ids for opensearch - search_objects_ids = queryset.values_list("id", flat=True) + search_objects_ids = list(queryset.values_list("id", flat=True)) query = self.kwargs.get("search", "") indices = [ From ffe554397b5d2ce2ad37d1553cb4f9211046453e Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 23 Sep 2026 09:27:40 +0200 Subject: [PATCH 05/11] feat(CrisalidDocument): optimize similars and --- apps/modules/__init__.py | 9 ++++++++- apps/modules/document.py | 14 ++++++++++++++ services/crisalid/models.py | 8 +++++++- services/crisalid/serializers.py | 22 +++++++++------------- services/crisalid/views.py | 2 +- 5 files changed, 39 insertions(+), 16 deletions(-) create mode 100644 apps/modules/document.py diff --git a/apps/modules/__init__.py b/apps/modules/__init__.py index 28119950..3c8ac6e8 100644 --- a/apps/modules/__init__.py +++ b/apps/modules/__init__.py @@ -1,6 +1,13 @@ +from .document import DocumentModules from .group import PeopleGroupModules from .project import ProjectModules from .tab import TabModules from .user import UserModules -__all__ = ["PeopleGroupModules", "ProjectModules", "TabModules", "UserModules"] +__all__ = [ + "PeopleGroupModules", + "ProjectModules", + "TabModules", + "UserModules", + "DocumentModules", +] diff --git a/apps/modules/document.py b/apps/modules/document.py new file mode 100644 index 00000000..b13ef7d5 --- /dev/null +++ b/apps/modules/document.py @@ -0,0 +1,14 @@ +from django.db.models import ( + QuerySet, +) + +from apps.modules.base import AbstractModules, register_module +from services.crisalid.models import Document + + +@register_module(Document) +class DocumentModules(AbstractModules): + instance: Document + + def similars(self) -> QuerySet[Document]: + return self.instance.similars() diff --git a/services/crisalid/models.py b/services/crisalid/models.py index cf0d6827..102c2496 100644 --- a/services/crisalid/models.py +++ b/services/crisalid/models.py @@ -6,7 +6,12 @@ from django.db.models.functions import Lower from django.utils.translation import gettext_lazy as _ -from apps.commons.mixins import HasEmbedding, HasOwner, OrganizationRelated +from apps.commons.mixins import ( + HasEmbedding, + HasOwner, + HasRelatedModules, + OrganizationRelated, +) from apps.organizations.models import Organization from services.crisalid.relators import RolesChoices from services.translator.mixins import HasAutoTranslatedFields @@ -173,6 +178,7 @@ class Meta: class Document( + HasRelatedModules, HasEmbedding, OrganizationRelated, HasAutoTranslatedFields, diff --git a/services/crisalid/serializers.py b/services/crisalid/serializers.py index 9b71db0d..184cd852 100644 --- a/services/crisalid/serializers.py +++ b/services/crisalid/serializers.py @@ -2,6 +2,8 @@ from apps.accounts.models import ProjectUser from apps.commons.fields import PrivacySettingProtectedMethodField +from apps.commons.serializers import PrivacySerializer +from apps.modules.serializers import ModulesSerializers from services.crisalid.models import Document, Identifier, Researcher from services.translator.serializers import auto_translated @@ -23,7 +25,7 @@ class Meta: exclude = ("id",) -class ResearcherSerializer(serializers.ModelSerializer): +class ResearcherSerializer(PrivacySerializer, serializers.ModelSerializer): user = ProjectUserMinimalSerializer() # TODO(remi): change privacy field for identifiers (not based in socials) identifiers = PrivacySettingProtectedMethodField(privacy_field="socials") @@ -53,25 +55,19 @@ class Meta(ResearcherSerializer.Meta): @auto_translated -class DocumentLightSerializer(serializers.ModelSerializer): - class Meta: - model = Document - fields = ("title", "publication_date", "document_type") - - -@auto_translated -class DocumentSerializer(DocumentLightSerializer): +class DocumentSerializer(ModulesSerializers, serializers.ModelSerializer): contributors = ResearcherDocumentsSerializer(many=True) identifiers = IdentifierSerializer(many=True) - similars = serializers.SerializerMethodField() class Meta: model = Document exclude = ("updated",) - def get_similars(self, instance: Document): - """return similar count""" - return instance.similars().count() + +class DocumentLightSerializer(DocumentSerializer): + class Meta(DocumentSerializer.Meta): + fields = ("title", "publication_date", "document_type", "modules") + modules_keys = () class DocumentAnalyticsSerializer(serializers.Serializer): diff --git a/services/crisalid/views.py b/services/crisalid/views.py index 8a6751ef..7f7d75ea 100644 --- a/services/crisalid/views.py +++ b/services/crisalid/views.py @@ -129,7 +129,7 @@ def get_queryset(self) -> QuerySet[Document]: def similars(self, request, *args, **kwargs): """methods to return similars projects""" obj: Document = self.get_object() - queryset = obj.similars() + queryset = obj.modules_by_user(request.user).similars() queryset_page = self.paginate_queryset(queryset) data = self.serializer_class( From 5c6728756c6658f18c9a942fa7cd98f49d113ac0 Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 23 Sep 2026 09:29:06 +0200 Subject: [PATCH 06/11] fix(PrivacySerializer): fix privacySettings not exists --- apps/commons/serializers.py | 16 ++++++++++--- locale/ca/LC_MESSAGES/django.po | 40 ++++++++++++++++----------------- locale/de/LC_MESSAGES/django.po | 40 ++++++++++++++++----------------- locale/en/LC_MESSAGES/django.po | 40 ++++++++++++++++----------------- locale/es/LC_MESSAGES/django.po | 40 ++++++++++++++++----------------- locale/et/LC_MESSAGES/django.po | 40 ++++++++++++++++----------------- locale/fr/LC_MESSAGES/django.po | 40 ++++++++++++++++----------------- locale/nl/LC_MESSAGES/django.po | 40 ++++++++++++++++----------------- 8 files changed, 153 insertions(+), 143 deletions(-) diff --git a/apps/commons/serializers.py b/apps/commons/serializers.py index 773101ca..09e25579 100644 --- a/apps/commons/serializers.py +++ b/apps/commons/serializers.py @@ -1,3 +1,4 @@ +import logging from collections.abc import Collection from functools import cache from typing import Any, Optional @@ -16,6 +17,8 @@ from apps.organizations.models import Organization from apps.projects.models import Project +logger = logging.getLogger(__name__) + class ProjectRelatedSerializer(serializers.ModelSerializer): """Base serializer for serializers related to projects.""" @@ -234,6 +237,10 @@ def _get_user(self, instance): return instance if isinstance(instance, HasOwner): return instance.get_owner() + + logger.warning( + "Invalid get user from privacySerializer: user=%r", type(instance) + ) return None def _privacy_settings( @@ -244,17 +251,20 @@ def _privacy_settings( if instance is None: return None, False, False + try: + settings = instance.privacy_settings + except ProjectUser.privacy_settings.RelatedObjectDoesNotExist: + # if user are not privacy_settings set, create a empty one (whitout save) + settings = PrivacySettings(user=instance) request = self.context.get("request") assert request is not None user: ProjectUser = request.user - settings = instance.privacy_settings - if user.is_anonymous: is_in_org = is_org_admin = False elif user.pk == instance.pk or user.is_superuser: - return settings, True, True + is_in_org = is_org_admin = True else: is_in_org = instance.groups.filter( organizations__isnull=False, diff --git a/locale/ca/LC_MESSAGES/django.po b/locale/ca/LC_MESSAGES/django.po index 53860ecc..b49a1c81 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-09-16 16:41+0200\n" +"POT-Creation-Date: 2026-09-23 09:19+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -176,21 +176,21 @@ msgstr "Estat de publicació desconegut" msgid "Unknown publication status '{publication_status}'" msgstr "Estat de publicació desconegut '{publication_status}'" -#: apps/commons/fields.py:32 apps/skills/serializers.py:89 +#: apps/commons/fields.py:27 apps/skills/serializers.py:89 msgid "This field is required." msgstr "Aquest camp és obligatori." -#: apps/commons/fields.py:33 +#: apps/commons/fields.py:28 #, python-brace-format msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "Id no vàlid \"{user_id}\" - l'objecte no existeix." -#: apps/commons/fields.py:35 apps/skills/serializers.py:94 +#: apps/commons/fields.py:30 apps/skills/serializers.py:94 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "Tipus incorrecte. S'esperava un valor str, s'ha rebut {data_type}." -#: apps/commons/serializers.py:210 +#: apps/commons/serializers.py:214 msgid "The value must be between -90 and 90." msgstr "" @@ -2619,63 +2619,63 @@ msgstr "" "\n" "Vols ser mentoritzat en %(skill)s? (recordatori)\n" -#: services/crisalid/models.py:189 +#: services/crisalid/models.py:198 msgid "Document" msgstr "" -#: services/crisalid/models.py:190 +#: services/crisalid/models.py:199 msgid "Scholarly Publication" msgstr "" -#: services/crisalid/models.py:191 +#: services/crisalid/models.py:200 msgid "Article" msgstr "" -#: services/crisalid/models.py:192 +#: services/crisalid/models.py:201 msgid "Journal Article" msgstr "" -#: services/crisalid/models.py:193 +#: services/crisalid/models.py:202 msgid "Conference Article" msgstr "Article de conferència" -#: services/crisalid/models.py:194 +#: services/crisalid/models.py:203 msgid "Conference Abstract" msgstr "" -#: services/crisalid/models.py:195 +#: services/crisalid/models.py:204 msgid "Preface" msgstr "" -#: services/crisalid/models.py:196 +#: services/crisalid/models.py:205 msgid "Comment" msgstr "Comentari" -#: services/crisalid/models.py:197 +#: services/crisalid/models.py:206 msgid "Book Chapter" msgstr "" -#: services/crisalid/models.py:198 +#: services/crisalid/models.py:207 msgid "Book" msgstr "" -#: services/crisalid/models.py:199 +#: services/crisalid/models.py:208 msgid "Monograph" msgstr "" -#: services/crisalid/models.py:200 +#: services/crisalid/models.py:209 msgid "Proceedings" msgstr "" -#: services/crisalid/models.py:201 +#: services/crisalid/models.py:210 msgid "Book Of Chapters" msgstr "" -#: services/crisalid/models.py:202 +#: services/crisalid/models.py:211 msgid "Presentation" msgstr "" -#: services/crisalid/models.py:203 +#: services/crisalid/models.py:212 msgid "Unknown" msgstr "" diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index bd3943ab..72469607 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-09-16 16:41+0200\n" +"POT-Creation-Date: 2026-09-23 09:19+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -178,22 +178,22 @@ msgstr "Unbekannter Veröffentlichungsstatus" msgid "Unknown publication status '{publication_status}'" msgstr "Unbekannter Veröffentlichungsstatus '{publication_status}'" -#: apps/commons/fields.py:32 apps/skills/serializers.py:89 +#: apps/commons/fields.py:27 apps/skills/serializers.py:89 msgid "This field is required." msgstr "Dieses Feld ist erforderlich." -#: apps/commons/fields.py:33 +#: apps/commons/fields.py:28 #, python-brace-format msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "Ungültige ID \"{user_id}\" – Objekt existiert nicht." -#: apps/commons/fields.py:35 apps/skills/serializers.py:94 +#: apps/commons/fields.py:30 apps/skills/serializers.py:94 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "" "Falscher Typ. Erwartet wurde ein String-Wert, erhalten wurde {data_type}." -#: apps/commons/serializers.py:210 +#: apps/commons/serializers.py:214 msgid "The value must be between -90 and 90." msgstr "" @@ -2647,63 +2647,63 @@ msgstr "" "\n" "Möchten Sie in %(skill)s betreut werden? (Erinnerung)\n" -#: services/crisalid/models.py:189 +#: services/crisalid/models.py:198 msgid "Document" msgstr "" -#: services/crisalid/models.py:190 +#: services/crisalid/models.py:199 msgid "Scholarly Publication" msgstr "" -#: services/crisalid/models.py:191 +#: services/crisalid/models.py:200 msgid "Article" msgstr "" -#: services/crisalid/models.py:192 +#: services/crisalid/models.py:201 msgid "Journal Article" msgstr "" -#: services/crisalid/models.py:193 +#: services/crisalid/models.py:202 msgid "Conference Article" msgstr "Konferenzartikel" -#: services/crisalid/models.py:194 +#: services/crisalid/models.py:203 msgid "Conference Abstract" msgstr "" -#: services/crisalid/models.py:195 +#: services/crisalid/models.py:204 msgid "Preface" msgstr "" -#: services/crisalid/models.py:196 +#: services/crisalid/models.py:205 msgid "Comment" msgstr "Kommentar" -#: services/crisalid/models.py:197 +#: services/crisalid/models.py:206 msgid "Book Chapter" msgstr "" -#: services/crisalid/models.py:198 +#: services/crisalid/models.py:207 msgid "Book" msgstr "" -#: services/crisalid/models.py:199 +#: services/crisalid/models.py:208 msgid "Monograph" msgstr "" -#: services/crisalid/models.py:200 +#: services/crisalid/models.py:209 msgid "Proceedings" msgstr "" -#: services/crisalid/models.py:201 +#: services/crisalid/models.py:210 msgid "Book Of Chapters" msgstr "" -#: services/crisalid/models.py:202 +#: services/crisalid/models.py:211 msgid "Presentation" msgstr "" -#: services/crisalid/models.py:203 +#: services/crisalid/models.py:212 msgid "Unknown" msgstr "" diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index 6a796cc5..4bb6c704 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-09-16 16:41+0200\n" +"POT-Creation-Date: 2026-09-23 09:19+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -154,21 +154,21 @@ msgstr "" msgid "Unknown publication status '{publication_status}'" msgstr "" -#: apps/commons/fields.py:32 apps/skills/serializers.py:89 +#: apps/commons/fields.py:27 apps/skills/serializers.py:89 msgid "This field is required." msgstr "" -#: apps/commons/fields.py:33 +#: apps/commons/fields.py:28 #, python-brace-format msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "" -#: apps/commons/fields.py:35 apps/skills/serializers.py:94 +#: apps/commons/fields.py:30 apps/skills/serializers.py:94 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "" -#: apps/commons/serializers.py:210 +#: apps/commons/serializers.py:214 msgid "The value must be between -90 and 90." msgstr "" @@ -1958,63 +1958,63 @@ msgid "" "Do you want to be mentored in %(skill)s? (reminder)\n" msgstr "" -#: services/crisalid/models.py:189 +#: services/crisalid/models.py:198 msgid "Document" msgstr "" -#: services/crisalid/models.py:190 +#: services/crisalid/models.py:199 msgid "Scholarly Publication" msgstr "" -#: services/crisalid/models.py:191 +#: services/crisalid/models.py:200 msgid "Article" msgstr "" -#: services/crisalid/models.py:192 +#: services/crisalid/models.py:201 msgid "Journal Article" msgstr "" -#: services/crisalid/models.py:193 +#: services/crisalid/models.py:202 msgid "Conference Article" msgstr "" -#: services/crisalid/models.py:194 +#: services/crisalid/models.py:203 msgid "Conference Abstract" msgstr "" -#: services/crisalid/models.py:195 +#: services/crisalid/models.py:204 msgid "Preface" msgstr "" -#: services/crisalid/models.py:196 +#: services/crisalid/models.py:205 msgid "Comment" msgstr "" -#: services/crisalid/models.py:197 +#: services/crisalid/models.py:206 msgid "Book Chapter" msgstr "" -#: services/crisalid/models.py:198 +#: services/crisalid/models.py:207 msgid "Book" msgstr "" -#: services/crisalid/models.py:199 +#: services/crisalid/models.py:208 msgid "Monograph" msgstr "" -#: services/crisalid/models.py:200 +#: services/crisalid/models.py:209 msgid "Proceedings" msgstr "" -#: services/crisalid/models.py:201 +#: services/crisalid/models.py:210 msgid "Book Of Chapters" msgstr "" -#: services/crisalid/models.py:202 +#: services/crisalid/models.py:211 msgid "Presentation" msgstr "" -#: services/crisalid/models.py:203 +#: services/crisalid/models.py:212 msgid "Unknown" msgstr "" diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po index 060aea8c..640221f7 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-09-16 16:41+0200\n" +"POT-Creation-Date: 2026-09-23 09:19+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -176,22 +176,22 @@ msgstr "Estado de publicación desconocido" msgid "Unknown publication status '{publication_status}'" msgstr "Estado de publicación desconocido '{publication_status}'" -#: apps/commons/fields.py:32 apps/skills/serializers.py:89 +#: apps/commons/fields.py:27 apps/skills/serializers.py:89 msgid "This field is required." msgstr "Este campo es obligatorio." -#: apps/commons/fields.py:33 +#: apps/commons/fields.py:28 #, python-brace-format msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "ID no válido \"{user_id}\" - el objeto no existe." -#: apps/commons/fields.py:35 apps/skills/serializers.py:94 +#: apps/commons/fields.py:30 apps/skills/serializers.py:94 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "" "Tipo incorrecto. Se esperaba un valor de cadena, se recibió {data_type}." -#: apps/commons/serializers.py:210 +#: apps/commons/serializers.py:214 msgid "The value must be between -90 and 90." msgstr "" @@ -2620,63 +2620,63 @@ msgstr "" "\n" "¿Quieres que te guíen en %(skill)s? (recordatorio)\n" -#: services/crisalid/models.py:189 +#: services/crisalid/models.py:198 msgid "Document" msgstr "" -#: services/crisalid/models.py:190 +#: services/crisalid/models.py:199 msgid "Scholarly Publication" msgstr "" -#: services/crisalid/models.py:191 +#: services/crisalid/models.py:200 msgid "Article" msgstr "" -#: services/crisalid/models.py:192 +#: services/crisalid/models.py:201 msgid "Journal Article" msgstr "" -#: services/crisalid/models.py:193 +#: services/crisalid/models.py:202 msgid "Conference Article" msgstr "Artículo de conferencia" -#: services/crisalid/models.py:194 +#: services/crisalid/models.py:203 msgid "Conference Abstract" msgstr "" -#: services/crisalid/models.py:195 +#: services/crisalid/models.py:204 msgid "Preface" msgstr "" -#: services/crisalid/models.py:196 +#: services/crisalid/models.py:205 msgid "Comment" msgstr "Comentario" -#: services/crisalid/models.py:197 +#: services/crisalid/models.py:206 msgid "Book Chapter" msgstr "" -#: services/crisalid/models.py:198 +#: services/crisalid/models.py:207 msgid "Book" msgstr "" -#: services/crisalid/models.py:199 +#: services/crisalid/models.py:208 msgid "Monograph" msgstr "" -#: services/crisalid/models.py:200 +#: services/crisalid/models.py:209 msgid "Proceedings" msgstr "" -#: services/crisalid/models.py:201 +#: services/crisalid/models.py:210 msgid "Book Of Chapters" msgstr "" -#: services/crisalid/models.py:202 +#: services/crisalid/models.py:211 msgid "Presentation" msgstr "" -#: services/crisalid/models.py:203 +#: services/crisalid/models.py:212 msgid "Unknown" msgstr "" diff --git a/locale/et/LC_MESSAGES/django.po b/locale/et/LC_MESSAGES/django.po index de8d2db0..8fb59ffa 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-09-16 16:41+0200\n" +"POT-Creation-Date: 2026-09-23 09:19+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -175,21 +175,21 @@ msgstr "Tundmatu avaldamise olek" msgid "Unknown publication status '{publication_status}'" msgstr "Tundmatu avaldamise olek '{publication_status}'" -#: apps/commons/fields.py:32 apps/skills/serializers.py:89 +#: apps/commons/fields.py:27 apps/skills/serializers.py:89 msgid "This field is required." msgstr "See väli on kohustuslik." -#: apps/commons/fields.py:33 +#: apps/commons/fields.py:28 #, python-brace-format msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "Vigane ID \"{user_id}\" - objekti ei eksisteeri." -#: apps/commons/fields.py:35 apps/skills/serializers.py:94 +#: apps/commons/fields.py:30 apps/skills/serializers.py:94 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "Vale tüüp. Oodati stringi väärtust, saadi {data_type}." -#: apps/commons/serializers.py:210 +#: apps/commons/serializers.py:214 msgid "The value must be between -90 and 90." msgstr "" @@ -2602,63 +2602,63 @@ msgstr "" "\n" "Kas soovite, et teid juhendataks %(skill)s valdkonnas? (meeldetuletus)\n" -#: services/crisalid/models.py:189 +#: services/crisalid/models.py:198 msgid "Document" msgstr "" -#: services/crisalid/models.py:190 +#: services/crisalid/models.py:199 msgid "Scholarly Publication" msgstr "" -#: services/crisalid/models.py:191 +#: services/crisalid/models.py:200 msgid "Article" msgstr "" -#: services/crisalid/models.py:192 +#: services/crisalid/models.py:201 msgid "Journal Article" msgstr "" -#: services/crisalid/models.py:193 +#: services/crisalid/models.py:202 msgid "Conference Article" msgstr "Konverentsiartikkel" -#: services/crisalid/models.py:194 +#: services/crisalid/models.py:203 msgid "Conference Abstract" msgstr "" -#: services/crisalid/models.py:195 +#: services/crisalid/models.py:204 msgid "Preface" msgstr "" -#: services/crisalid/models.py:196 +#: services/crisalid/models.py:205 msgid "Comment" msgstr "Kommentaar" -#: services/crisalid/models.py:197 +#: services/crisalid/models.py:206 msgid "Book Chapter" msgstr "" -#: services/crisalid/models.py:198 +#: services/crisalid/models.py:207 msgid "Book" msgstr "" -#: services/crisalid/models.py:199 +#: services/crisalid/models.py:208 msgid "Monograph" msgstr "" -#: services/crisalid/models.py:200 +#: services/crisalid/models.py:209 msgid "Proceedings" msgstr "" -#: services/crisalid/models.py:201 +#: services/crisalid/models.py:210 msgid "Book Of Chapters" msgstr "" -#: services/crisalid/models.py:202 +#: services/crisalid/models.py:211 msgid "Presentation" msgstr "" -#: services/crisalid/models.py:203 +#: services/crisalid/models.py:212 msgid "Unknown" msgstr "" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 3f5557be..698b6be0 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-09-16 16:41+0200\n" +"POT-Creation-Date: 2026-09-23 09:19+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -178,21 +178,21 @@ msgstr "Statut de publication inconnu" msgid "Unknown publication status '{publication_status}'" msgstr "Statut de publication inconnu '{publication_status}'" -#: apps/commons/fields.py:32 apps/skills/serializers.py:89 +#: apps/commons/fields.py:27 apps/skills/serializers.py:89 msgid "This field is required." msgstr "Ce champ est obligatoire." -#: apps/commons/fields.py:33 +#: apps/commons/fields.py:28 #, python-brace-format msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "identifiant invalide \"{user_id}\" - cet objet n'existe pas." -#: apps/commons/fields.py:35 apps/skills/serializers.py:94 +#: apps/commons/fields.py:30 apps/skills/serializers.py:94 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "Type incorrect. Valeur str attendue, {data_type} reçue." -#: apps/commons/serializers.py:210 +#: apps/commons/serializers.py:214 msgid "The value must be between -90 and 90." msgstr "" @@ -2622,63 +2622,63 @@ msgstr "" "\n" "Voulez-vous être mentoré·e en %(skill)s? (rappel)\n" -#: services/crisalid/models.py:189 +#: services/crisalid/models.py:198 msgid "Document" msgstr "" -#: services/crisalid/models.py:190 +#: services/crisalid/models.py:199 msgid "Scholarly Publication" msgstr "" -#: services/crisalid/models.py:191 +#: services/crisalid/models.py:200 msgid "Article" msgstr "" -#: services/crisalid/models.py:192 +#: services/crisalid/models.py:201 msgid "Journal Article" msgstr "" -#: services/crisalid/models.py:193 +#: services/crisalid/models.py:202 msgid "Conference Article" msgstr "Article de conférence" -#: services/crisalid/models.py:194 +#: services/crisalid/models.py:203 msgid "Conference Abstract" msgstr "" -#: services/crisalid/models.py:195 +#: services/crisalid/models.py:204 msgid "Preface" msgstr "" -#: services/crisalid/models.py:196 +#: services/crisalid/models.py:205 msgid "Comment" msgstr "Commentaire" -#: services/crisalid/models.py:197 +#: services/crisalid/models.py:206 msgid "Book Chapter" msgstr "" -#: services/crisalid/models.py:198 +#: services/crisalid/models.py:207 msgid "Book" msgstr "" -#: services/crisalid/models.py:199 +#: services/crisalid/models.py:208 msgid "Monograph" msgstr "" -#: services/crisalid/models.py:200 +#: services/crisalid/models.py:209 msgid "Proceedings" msgstr "" -#: services/crisalid/models.py:201 +#: services/crisalid/models.py:210 msgid "Book Of Chapters" msgstr "" -#: services/crisalid/models.py:202 +#: services/crisalid/models.py:211 msgid "Presentation" msgstr "" -#: services/crisalid/models.py:203 +#: services/crisalid/models.py:212 msgid "Unknown" msgstr "" diff --git a/locale/nl/LC_MESSAGES/django.po b/locale/nl/LC_MESSAGES/django.po index 9fca8844..95f7f3a8 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-09-16 16:41+0200\n" +"POT-Creation-Date: 2026-09-23 09:19+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -178,21 +178,21 @@ msgstr "Onbekende publicatiestatus" msgid "Unknown publication status '{publication_status}'" msgstr "Onbekende publicatiestatus '{publication_status}'" -#: apps/commons/fields.py:32 apps/skills/serializers.py:89 +#: apps/commons/fields.py:27 apps/skills/serializers.py:89 msgid "This field is required." msgstr "Dit veld is verplicht." -#: apps/commons/fields.py:33 +#: apps/commons/fields.py:28 #, python-brace-format msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "Ongeldige id \"{user_id}\" - object bestaat niet." -#: apps/commons/fields.py:35 apps/skills/serializers.py:94 +#: apps/commons/fields.py:30 apps/skills/serializers.py:94 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "Onjuist type. Verwachte stringwaarde, ontvangen {data_type}." -#: apps/commons/serializers.py:210 +#: apps/commons/serializers.py:214 msgid "The value must be between -90 and 90." msgstr "" @@ -2636,63 +2636,63 @@ msgstr "" "\n" "Wil je begeleid worden in %(skill)s? (herinnering)\n" -#: services/crisalid/models.py:189 +#: services/crisalid/models.py:198 msgid "Document" msgstr "" -#: services/crisalid/models.py:190 +#: services/crisalid/models.py:199 msgid "Scholarly Publication" msgstr "" -#: services/crisalid/models.py:191 +#: services/crisalid/models.py:200 msgid "Article" msgstr "" -#: services/crisalid/models.py:192 +#: services/crisalid/models.py:201 msgid "Journal Article" msgstr "" -#: services/crisalid/models.py:193 +#: services/crisalid/models.py:202 msgid "Conference Article" msgstr "Conferentieartikel" -#: services/crisalid/models.py:194 +#: services/crisalid/models.py:203 msgid "Conference Abstract" msgstr "" -#: services/crisalid/models.py:195 +#: services/crisalid/models.py:204 msgid "Preface" msgstr "" -#: services/crisalid/models.py:196 +#: services/crisalid/models.py:205 msgid "Comment" msgstr "Commentaar" -#: services/crisalid/models.py:197 +#: services/crisalid/models.py:206 msgid "Book Chapter" msgstr "" -#: services/crisalid/models.py:198 +#: services/crisalid/models.py:207 msgid "Book" msgstr "" -#: services/crisalid/models.py:199 +#: services/crisalid/models.py:208 msgid "Monograph" msgstr "" -#: services/crisalid/models.py:200 +#: services/crisalid/models.py:209 msgid "Proceedings" msgstr "" -#: services/crisalid/models.py:201 +#: services/crisalid/models.py:210 msgid "Book Of Chapters" msgstr "" -#: services/crisalid/models.py:202 +#: services/crisalid/models.py:211 msgid "Presentation" msgstr "" -#: services/crisalid/models.py:203 +#: services/crisalid/models.py:212 msgid "Unknown" msgstr "" From 0f3be9937d070a1a75cefe5eab2541931029cae6 Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 23 Sep 2026 10:01:32 +0200 Subject: [PATCH 07/11] i18n --- locale/ca/LC_MESSAGES/django.po | 4 ++-- locale/de/LC_MESSAGES/django.po | 4 ++-- locale/en/LC_MESSAGES/django.po | 4 ++-- locale/es/LC_MESSAGES/django.po | 4 ++-- locale/et/LC_MESSAGES/django.po | 4 ++-- locale/fr/LC_MESSAGES/django.po | 4 ++-- locale/nl/LC_MESSAGES/django.po | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/locale/ca/LC_MESSAGES/django.po b/locale/ca/LC_MESSAGES/django.po index b49a1c81..3dc3e479 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-09-23 09:19+0200\n" +"POT-Creation-Date: 2026-09-23 10:01+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -190,7 +190,7 @@ msgstr "Id no vàlid \"{user_id}\" - l'objecte no existeix." msgid "Incorrect type. Expected str value, received {data_type}." msgstr "Tipus incorrecte. S'esperava un valor str, s'ha rebut {data_type}." -#: apps/commons/serializers.py:214 +#: apps/commons/serializers.py:217 msgid "The value must be between -90 and 90." msgstr "" diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index 72469607..f0902d5d 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-09-23 09:19+0200\n" +"POT-Creation-Date: 2026-09-23 10:01+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -193,7 +193,7 @@ msgid "Incorrect type. Expected str value, received {data_type}." msgstr "" "Falscher Typ. Erwartet wurde ein String-Wert, erhalten wurde {data_type}." -#: apps/commons/serializers.py:214 +#: apps/commons/serializers.py:217 msgid "The value must be between -90 and 90." msgstr "" diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index 4bb6c704..5d0c2c92 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-09-23 09:19+0200\n" +"POT-Creation-Date: 2026-09-23 10:01+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -168,7 +168,7 @@ msgstr "" msgid "Incorrect type. Expected str value, received {data_type}." msgstr "" -#: apps/commons/serializers.py:214 +#: apps/commons/serializers.py:217 msgid "The value must be between -90 and 90." msgstr "" diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po index 640221f7..a273003f 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-09-23 09:19+0200\n" +"POT-Creation-Date: 2026-09-23 10:01+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -191,7 +191,7 @@ msgid "Incorrect type. Expected str value, received {data_type}." msgstr "" "Tipo incorrecto. Se esperaba un valor de cadena, se recibió {data_type}." -#: apps/commons/serializers.py:214 +#: apps/commons/serializers.py:217 msgid "The value must be between -90 and 90." msgstr "" diff --git a/locale/et/LC_MESSAGES/django.po b/locale/et/LC_MESSAGES/django.po index 8fb59ffa..8f47ac67 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-09-23 09:19+0200\n" +"POT-Creation-Date: 2026-09-23 10:01+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -189,7 +189,7 @@ msgstr "Vigane ID \"{user_id}\" - objekti ei eksisteeri." msgid "Incorrect type. Expected str value, received {data_type}." msgstr "Vale tüüp. Oodati stringi väärtust, saadi {data_type}." -#: apps/commons/serializers.py:214 +#: apps/commons/serializers.py:217 msgid "The value must be between -90 and 90." msgstr "" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 698b6be0..60a2605a 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-09-23 09:19+0200\n" +"POT-Creation-Date: 2026-09-23 10:01+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -192,7 +192,7 @@ msgstr "identifiant invalide \"{user_id}\" - cet objet n'existe pas." msgid "Incorrect type. Expected str value, received {data_type}." msgstr "Type incorrect. Valeur str attendue, {data_type} reçue." -#: apps/commons/serializers.py:214 +#: apps/commons/serializers.py:217 msgid "The value must be between -90 and 90." msgstr "" diff --git a/locale/nl/LC_MESSAGES/django.po b/locale/nl/LC_MESSAGES/django.po index 95f7f3a8..5762a8bc 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-09-23 09:19+0200\n" +"POT-Creation-Date: 2026-09-23 10:01+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -192,7 +192,7 @@ msgstr "Ongeldige id \"{user_id}\" - object bestaat niet." msgid "Incorrect type. Expected str value, received {data_type}." msgstr "Onjuist type. Verwachte stringwaarde, ontvangen {data_type}." -#: apps/commons/serializers.py:214 +#: apps/commons/serializers.py:217 msgid "The value must be between -90 and 90." msgstr "" From 41bdec5a7267df4f78424f8c7f21c11943569211 Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 23 Sep 2026 11:14:38 +0200 Subject: [PATCH 08/11] test: fix organizations/request missing in serializer --- apps/commons/serializers.py | 6 +++++- apps/feedbacks/views.py | 5 +++++ apps/skills/views.py | 6 +++++- services/mistral/views.py | 22 ++++++++++++---------- 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/apps/commons/serializers.py b/apps/commons/serializers.py index 09e25579..b0666c4d 100644 --- a/apps/commons/serializers.py +++ b/apps/commons/serializers.py @@ -258,7 +258,11 @@ def _privacy_settings( settings = PrivacySettings(user=instance) request = self.context.get("request") - assert request is not None + + if request is None: + logger.warning("Request is not set in serialier %r", type(self)) + return None, False, False + user: ProjectUser = request.user if user.is_anonymous: diff --git a/apps/feedbacks/views.py b/apps/feedbacks/views.py index 36c9379d..b420ecf3 100644 --- a/apps/feedbacks/views.py +++ b/apps/feedbacks/views.py @@ -48,6 +48,11 @@ class ReviewViewSet(MultipleIDViewsetMixin, viewsets.ModelViewSet): lookup_value_regex = "[0-9]+" multiple_lookup_fields = [(ProjectUser, "user_id"), (Project, "project_id")] + def get_serializer_context(self): + context = super().get_serializer_context() + context["request"] = self.request + return context + def get_permissions(self): codename = map_action_to_permission(self.action, "review") if codename: diff --git a/apps/skills/views.py b/apps/skills/views.py index a3f26f8f..f5ca93f1 100644 --- a/apps/skills/views.py +++ b/apps/skills/views.py @@ -816,4 +816,8 @@ def respond(self, request, *args, **kwargs): organization=organization, **serializer.validated_data, ) - return Response(MentoringSerializer(instance).data) + return Response( + MentoringSerializer( + instance, context={"request": request, "organization": organization} + ).data + ) diff --git a/services/mistral/views.py b/services/mistral/views.py index e6d21f81..8c736bf7 100644 --- a/services/mistral/views.py +++ b/services/mistral/views.py @@ -12,14 +12,15 @@ from apps.commons.cache import redis_cache_viewset_method from apps.commons.permissions import ReadOnly from apps.commons.views import MultipleIDViewsetMixin, NestedOrganizationViewMixins -from apps.organizations.utils import get_below_hierarchy_codes from apps.projects.models import Project from apps.projects.serializers import ProjectLightSerializer from .models import ProjectEmbedding, UserEmbedding -class RecommendationsViewset(MultipleIDViewsetMixin, GenericViewSet): +class RecommendationsViewset( + NestedOrganizationViewMixins, MultipleIDViewsetMixin, GenericViewSet +): filter_backends = [DjangoFilterBackend] ordering_fields = [] permission_classes = [ReadOnly] @@ -28,6 +29,11 @@ class RecommendationsViewset(MultipleIDViewsetMixin, GenericViewSet): queryset: QuerySet[Project | ProjectUser] serializer_class: ProjectLightSerializer | UserLightSerializer + def get_serializer_context(self): + context = super().get_serializer_context() + context["request"] = self.request + return context + def _list(self, request, *args, **kwargs): """ Redefinition of the ListModelMixin list method to allow for pagination. @@ -86,9 +92,7 @@ def get_queryset(self) -> QuerySet[Project | ProjectUser]: project = get_object_or_404( self.request.user.get_project_queryset(), id=self.kwargs["project_id"], - organizations__code__in=get_below_hierarchy_codes( - [self.kwargs["organization_code"]] - ), + organizations__in=self.organizations, ) return self.get_queryset_for_project(project) return self.get_queryset_for_user(self.request.user) @@ -190,9 +194,7 @@ def random_user_recommendations(self, request, *args, **kwargs): return Response(serializer.data) -class ProjectRecommendationsViewset( - NestedOrganizationViewMixins, RecommendationsViewset -): +class ProjectRecommendationsViewset(RecommendationsViewset): queryset = Project.objects.all() serializer_class = ProjectLightSerializer @@ -219,7 +221,6 @@ def get_queryset_for_user(self, user: ProjectUser) -> QuerySet[Project]: score__activity__gte=0.37, # 6 months of inactivity ) .prefetch_related("categories") - .select_related("privacy_settings") ) embedding = self.get_user_embedding(user) if user.is_authenticated: @@ -229,7 +230,7 @@ def get_queryset_for_user(self, user: ProjectUser) -> QuerySet[Project]: return queryset.order_by("-score__score") -class UserRecommendationsViewset(NestedOrganizationViewMixins, RecommendationsViewset): +class UserRecommendationsViewset(RecommendationsViewset): queryset = ProjectUser.objects.all() serializer_class = UserLightSerializer @@ -241,6 +242,7 @@ def get_queryset_for_project(self, project: Project) -> QuerySet[ProjectUser]: score__activity__gte=0.1, # 49 weeks of inactivity ) .exclude(groups__projects__id=project.id) + .select_related("privacy_settings") ) embedding = self.get_project_embedding(project) if self.request.user.is_authenticated: From f05fae4b53f1a93805aee451d6f21f221b2d30e3 Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 23 Sep 2026 11:31:12 +0200 Subject: [PATCH 09/11] test: fix organizations/request missing in serializer --- apps/projects/serializers.py | 39 ++++++++++++++---------------------- apps/projects/views.py | 5 +++++ apps/skills/views.py | 12 +++++++++-- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/apps/projects/serializers.py b/apps/projects/serializers.py index 099ff4d0..cc31fa22 100644 --- a/apps/projects/serializers.py +++ b/apps/projects/serializers.py @@ -724,16 +724,13 @@ class ProjectVersionSerializer(serializers.ModelSerializer): linked_projects = serializers.SerializerMethodField(read_only=True) delta = serializers.SerializerMethodField(read_only=True) - @staticmethod - def get_id(version) -> int: + def get_id(self, version) -> int: return version.pk - @staticmethod - def get_project_id(version) -> str: + def get_project_id(self, version) -> str: return version.id - @staticmethod - def get_delta(version) -> dict[str, str]: + def get_delta(self, version) -> dict[str, str]: previous = version.prev_record while previous: previous_reason = previous.history_change_reason @@ -756,8 +753,7 @@ def get_delta(version) -> dict[str, str]: previous = previous.prev_record return {} - @staticmethod - def get_categories(version) -> list[str]: + def get_categories(self, version) -> list[str]: categories_ids = version.categories.all().values_list( "projectcategory_id", flat=True ) @@ -765,29 +761,27 @@ def get_categories(version) -> list[str]: "name", flat=True ) - @staticmethod - def get_tags(version) -> list[str]: + def get_tags(self, version) -> list[str]: tags_ids = version.tags.all().values_list("tag_id", flat=True) return Tag.objects.filter(id__in=tags_ids).values_list("title", flat=True) - @staticmethod - def get_members(version) -> list[str]: + def get_members(self, version) -> list[str]: members = Project.objects.get(id=version.id).get_all_members() return [m.get_full_name() for m in members] - @staticmethod - def get_comments(version) -> dict[str, Any]: + def get_comments(self, version) -> dict[str, Any]: comments = Comment.history.as_of(version.history_date).filter( project__id=version.id, deleted_at=None ) - return CommentSerializer(comments, many=True).data + return CommentSerializer(comments, many=True, context=self.context).data - @staticmethod - def get_linked_projects(version) -> dict[str, Any]: + def get_linked_projects(self, version) -> dict[str, Any]: linked_projects = LinkedProject.history.as_of(version.history_date).filter( target__id=version.id ) - return LinkedProjectSerializer(linked_projects, many=True).data + return LinkedProjectSerializer( + linked_projects, many=True, context=self.context + ).data class Meta: model = apps.get_model("projects", "HistoricalProject") @@ -813,16 +807,13 @@ class ProjectVersionListSerializer(serializers.ModelSerializer): project_id = serializers.SerializerMethodField(read_only=True) updated_fields = serializers.SerializerMethodField(read_only=True) - @staticmethod - def get_id(version) -> int: + def get_id(self, version) -> int: return version.pk - @staticmethod - def get_project_id(version) -> str: + def get_project_id(self, version) -> str: return version.id - @staticmethod - def get_updated_fields(version) -> list[str]: + def get_updated_fields(self, version) -> list[str]: previous = version.prev_record while previous: previous_reason = previous.history_change_reason diff --git a/apps/projects/views.py b/apps/projects/views.py index 0a2f442c..16264465 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -608,6 +608,11 @@ class HistoricalProjectViewSet(NestedProjectViewMixins, viewsets.ReadOnlyModelVi lookup_field = "pk" permission_classes = [ReadOnly] + def get_serializer_context(self): + context = super().get_serializer_context() + context["request"] = self.request + return context + def get_serializer_class(self): if self.action == "list": return ProjectVersionListSerializer diff --git a/apps/skills/views.py b/apps/skills/views.py index f5ca93f1..2862a737 100644 --- a/apps/skills/views.py +++ b/apps/skills/views.py @@ -714,7 +714,11 @@ def contact_mentor(self, request, *args, **kwargs): organization=organization, **serializer.validated_data, ) - return Response(MentoringSerializer(instance).data) + return Response( + MentoringSerializer( + instance, context={"request": request, "organization": organization} + ).data + ) @extend_schema( request=MentoringContactSerializer, @@ -765,7 +769,11 @@ def contact_mentoree(self, request, *args, **kwargs): organization=organization, **serializer.validated_data, ) - return Response(MentoringSerializer(instance).data) + return Response( + MentoringSerializer( + instance, context={"request": request, "organization": organization} + ).data + ) @extend_schema( request=MentoringResponseSerializer, From 1cf2e42bd55463eae8d350b9714b106d37b5003a Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 23 Sep 2026 11:34:43 +0200 Subject: [PATCH 10/11] test: fix annonymous user in subserializer --- apps/accounts/serializers.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/apps/accounts/serializers.py b/apps/accounts/serializers.py index 16ec64ce..347e6a60 100644 --- a/apps/accounts/serializers.py +++ b/apps/accounts/serializers.py @@ -1,4 +1,5 @@ import uuid +from functools import cached_property from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType @@ -40,6 +41,7 @@ UserRolePermissionDeniedError, ) from .models import ( + AnonymousUser, PeopleGroup, PeopleGroupLocation, PrivacySettings, @@ -388,6 +390,24 @@ def to_internal_value(self, data): return super().to_internal_value(data) + @cached_property + def _user_acces(self): + request = self.context.get("request") + if request: + return request.user.get_user_queryset().values_list("pk", flat=True) + return [] + + def to_representation(self, instance: ProjectUser): + # TODO(remi): optimize this + force_display = self.context.get("force_display", False) + if force_display or instance.pk in self._user_acces: + return super().to_representation(instance) + + return { + **AnonymousUser.serialize(with_permissions=False), + "current_org_role": None, + } + @auto_translated class UserLightSerializer(UserSerializer): From 49b596fdbd91514b762099cfb10049360b573656 Mon Sep 17 00:00:00 2001 From: rgermain Date: Thu, 24 Sep 2026 10:49:14 +0200 Subject: [PATCH 11/11] fix: remove unused --- apps/feedbacks/views.py | 5 ----- apps/projects/views.py | 5 ----- 2 files changed, 10 deletions(-) diff --git a/apps/feedbacks/views.py b/apps/feedbacks/views.py index b420ecf3..36c9379d 100644 --- a/apps/feedbacks/views.py +++ b/apps/feedbacks/views.py @@ -48,11 +48,6 @@ class ReviewViewSet(MultipleIDViewsetMixin, viewsets.ModelViewSet): lookup_value_regex = "[0-9]+" multiple_lookup_fields = [(ProjectUser, "user_id"), (Project, "project_id")] - def get_serializer_context(self): - context = super().get_serializer_context() - context["request"] = self.request - return context - def get_permissions(self): codename = map_action_to_permission(self.action, "review") if codename: diff --git a/apps/projects/views.py b/apps/projects/views.py index 16264465..0a2f442c 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -608,11 +608,6 @@ class HistoricalProjectViewSet(NestedProjectViewMixins, viewsets.ReadOnlyModelVi lookup_field = "pk" permission_classes = [ReadOnly] - def get_serializer_context(self): - context = super().get_serializer_context() - context["request"] = self.request - return context - def get_serializer_class(self): if self.action == "list": return ProjectVersionListSerializer