From 63a4dfcc5df5cd9d3355f8834dc8af9ca97dc375 Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 2 Sep 2026 16:43:16 +0200 Subject: [PATCH 01/25] refactor(Category): rework category following view page --- apps/modules/user.py | 8 +++++++ apps/organizations/serializers.py | 39 +++++++++++++++++++++++++++++++ services/crisalid/views.py | 4 +++- 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/apps/modules/user.py b/apps/modules/user.py index 25ee8a5a2..f01841455 100644 --- a/apps/modules/user.py +++ b/apps/modules/user.py @@ -5,6 +5,7 @@ ) from apps.accounts.models import PeopleGroup, ProjectUser +from apps.commons.models import GroupData from apps.files.models import ProjectUserAttachmentFile, ProjectUserAttachmentLink from apps.modules.base import AbstractModules, organization_related, register_module from apps.notifications.models import Notification @@ -60,6 +61,13 @@ def projects(self) -> QuerySet[Project]: .distinct() ) + @organization_related + def reviews_projects(self) -> QuerySet[Project]: + return self.user.get_project_queryset().filter( + groups__data__role=GroupData.Role.REVIEWERS, + groups__users=self.instance, + ) + @organization_related def notifications(self) -> QuerySet[Notification]: return self.instance.notifications_received.filter(is_viewed=False) diff --git a/apps/organizations/serializers.py b/apps/organizations/serializers.py index 1f1e045d8..9f85a9549 100644 --- a/apps/organizations/serializers.py +++ b/apps/organizations/serializers.py @@ -434,6 +434,8 @@ class ProjectCategoryLightSerializer( ): organization = SlugRelatedField(read_only=True, slug_field="code") + is_followed = serializers.SerializerMethodField() + class Meta: model = ProjectCategory fields = [ @@ -444,8 +446,19 @@ class Meta: "foreground_color", "organization", "is_reviewable", + "is_followed", ] + def get_is_followed(self, category: ProjectCategory) -> dict[str, Any]: + if "request" in self.context: + user = self.context["request"].user + if not user.is_anonymous: + follow = CategoryFollow.objects.filter(follower=user, category=category) + user_follow = follow.first() + if user_follow: + return {"is_followed": True, "follow_id": user_follow.id} + return {"is_followed": False, "follow_id": None} + def get_related_organizations(self) -> list[Organization]: self.is_valid(raise_exception=True) return [ProjectCategory.objects.get(id=self.validated_data["id"]).organization] @@ -616,6 +629,8 @@ class ProjectCategoryHierarchySerializer( children = serializers.SerializerMethodField() background_image = ImageSerializer(read_only=True) + is_followed = serializers.SerializerMethodField() + class Meta: model = ProjectCategory read_only_fields = [ @@ -626,9 +641,20 @@ class Meta: "foreground_color", "background_image", "children", + "is_followed", ] fields = read_only_fields + def get_is_followed(self, category: ProjectCategory) -> dict[str, Any]: + if "request" in self.context: + user = self.context["request"].user + if not user.is_anonymous: + follow = CategoryFollow.objects.filter(follower=user, category=category) + user_follow = follow.first() + if user_follow: + return {"is_followed": True, "follow_id": user_follow.id} + return {"is_followed": False, "follow_id": None} + def get_children(self, category: ProjectCategory) -> list[dict[str, str | int]]: context = self.context mapping = context.get("mapping") @@ -690,6 +716,8 @@ class ProjectCategorySerializer( source="templates", ) + is_followed = serializers.SerializerMethodField() + class Meta: model = ProjectCategory read_only_fields = [ @@ -697,6 +725,7 @@ class Meta: "organization", "background_image", "templates", + "is_followed", ] fields = read_only_fields + [ "id", @@ -717,6 +746,16 @@ class Meta: "templates_ids", ] + def get_is_followed(self, category: ProjectCategory) -> dict[str, Any]: + if "request" in self.context: + user = self.context["request"].user + if not user.is_anonymous: + follow = CategoryFollow.objects.filter(follower=user, category=category) + user_follow = follow.first() + if user_follow: + return {"is_followed": True, "follow_id": user_follow.id} + return {"is_followed": False, "follow_id": None} + def get_hierarchy(self, obj: ProjectCategory) -> list[dict[str, str | int]]: hierarchy = [] while obj.parent and not obj.parent.is_root: diff --git a/services/crisalid/views.py b/services/crisalid/views.py index 3138bf5eb..8a6751efc 100644 --- a/services/crisalid/views.py +++ b/services/crisalid/views.py @@ -119,7 +119,9 @@ def filter_queryset( def get_queryset(self) -> QuerySet[Document]: return ( Document.objects.filter(document_type__in=self.document_types) - .prefetch_related("identifiers", "contributors__user") + .prefetch_related( + "identifiers", "contributors__user", "contributors__identifiers" + ) .order_by("-publication_date") ) From f751f44ead0d1f6b063747c809633cee686e0adc Mon Sep 17 00:00:00 2001 From: rgermain Date: Fri, 4 Sep 2026 14:08:01 +0200 Subject: [PATCH 02/25] clean serializers --- apps/organizations/models.py | 3 + apps/organizations/serializers.py | 336 ++++++++++++++---------------- apps/organizations/views.py | 58 +++--- services/translator/interface.py | 3 +- services/translator/testcases.py | 3 +- 5 files changed, 195 insertions(+), 208 deletions(-) diff --git a/apps/organizations/models.py b/apps/organizations/models.py index 263286309..fe9c71254 100644 --- a/apps/organizations/models.py +++ b/apps/organizations/models.py @@ -16,6 +16,7 @@ OrganizationRelated, ) from apps.commons.models import GroupData +from apps.commons.queryset import MultipleIdsQuerySet from apps.commons.utils import ( get_permissions_from_subscopes, get_write_permissions_from_subscopes, @@ -593,6 +594,8 @@ class ProjectCategory( ) history = HistoricalRecords() + objects = MultipleIdsQuerySet.as_manager() + class Meta: ordering = ["organization__code", "order_index"] diff --git a/apps/organizations/serializers.py b/apps/organizations/serializers.py index 9f85a9549..adcf20e5f 100644 --- a/apps/organizations/serializers.py +++ b/apps/organizations/serializers.py @@ -420,50 +420,6 @@ def get_related_organizations(self) -> list[Organization]: return [self.instance.organization] if self.instance else [] -@auto_translated -class ProjectCategorySuperLightSerializer(serializers.ModelSerializer): - class Meta: - model = ProjectCategory - fields = ["id", "slug", "name"] - - -@auto_translated -class ProjectCategoryLightSerializer( - OrganizationRelatedSerializer, - serializers.ModelSerializer, -): - organization = SlugRelatedField(read_only=True, slug_field="code") - - is_followed = serializers.SerializerMethodField() - - class Meta: - model = ProjectCategory - fields = [ - "id", - "slug", - "name", - "background_color", - "foreground_color", - "organization", - "is_reviewable", - "is_followed", - ] - - def get_is_followed(self, category: ProjectCategory) -> dict[str, Any]: - if "request" in self.context: - user = self.context["request"].user - if not user.is_anonymous: - follow = CategoryFollow.objects.filter(follower=user, category=category) - user_follow = follow.first() - if user_follow: - return {"is_followed": True, "follow_id": user_follow.id} - return {"is_followed": False, "follow_id": None} - - def get_related_organizations(self) -> list[Organization]: - self.is_valid(raise_exception=True) - return [ProjectCategory.objects.get(id=self.validated_data["id"]).organization] - - class TemplateTabSerializer(StringsImagesSerializer, serializers.ModelSerializer): string_images_fields: list[str] = [ "content", @@ -488,139 +444,6 @@ class Meta: ) -@auto_translated -class TemplateSerializer( - StringsImagesSerializer, - OrganizationRelatedSerializer, - serializers.ModelSerializer, -): - string_images_fields: list[str] = [ - "description", - "project_description", - "blogentry_content", - "comment_content", - ] - string_images_forbid_fields: list[str] = [ - "name", - "project_title", - "project_purpose", - "goal_title", - "goal_description", - "review_title", - "review_description", - ] - string_images_upload_to: str = "template/images/" - string_images_view: str = "Template-images-detail" - - project_tags = TagRelatedField(many=True, required=False) - organization = SlugRelatedField(read_only=True, slug_field="code") - categories = ProjectCategoryLightSerializer(many=True, read_only=True) - # write-only - categories_ids = serializers.PrimaryKeyRelatedField( - many=True, - required=False, - write_only=True, - queryset=ProjectCategory.objects.all(), - source="categories", - ) - tabs = TemplateTabSerializer(many=True) - - class Meta: - model = Template - read_only_fields = ["id", "organization", "categories"] - fields = read_only_fields + [ - "name", - "description", - "language", - "project_title", - "project_description", - "project_purpose", - "project_tags", - "blogentry_title", - "blogentry_content", - "goal_title", - "goal_description", - "review_title", - "review_description", - "comment_content", - "categories_ids", - "tabs", - "enable_tab", - ] - - def get_related_organizations(self) -> list[Organization]: - """Retrieve the related organizations""" - return [self.validated_data.get("organization", [])] - - def get_string_images_kwargs( - self, instance: Template, field_name: str, *args: Any, **kwargs: Any - ) -> dict[str, Any]: - """Get additional kwargs for image processing based on the instance.""" - return { - "organization_code": instance.organization.code, - "template_id": instance.id, - } - - @transaction.atomic - def update(self, instance, validated_data): - tabs_data = validated_data.pop("tabs", None) - - # Update du Template - instance = super().update(instance, validated_data) - - if tabs_data is None: - return instance - - existing_tabs = {tab.id: tab for tab in instance.tabs.all()} - received_ids = set() - - for tab_data in tabs_data: - tab_id = tab_data.pop("id", None) - tab_data["template"] = instance - - tab = existing_tabs.get(tab_id, TemplateTab(**tab_data)) - - for field, value in tab_data.items(): - setattr(tab, field, value) - - tab.save() - received_ids.add(tab.id) - - to_delete = [tab_id for tab_id in existing_tabs if tab_id not in received_ids] - TemplateTab.objects.filter(id__in=to_delete).delete() - - return instance - - @transaction.atomic - def create(self, validated_data): - tabs_data = validated_data.pop("tabs", []) - - template = super().create(validated_data) - - for tab_data in tabs_data: - tab_data["template"] = template - tab = TemplateTab(**tab_data) - tab.save() - - return template - - -class ProjectTemplateSerializer(TemplateSerializer): - class Meta(TemplateSerializer.Meta): - # remove unused field - read_only_fields = [ - field - for field in TemplateSerializer.Meta.fields - if field - not in ( - "categories_ids", - "organization", - "categories", - ) - ] - fields = read_only_fields - - @auto_translated class ProjectCategoryHierarchySerializer( OrganizationRelatedSerializer, @@ -679,6 +502,9 @@ def get_children(self, category: ProjectCategory) -> list[dict[str, str | int]]: ).data +# project category + + @auto_translated class ProjectCategorySerializer( StringsImagesSerializer, @@ -805,6 +631,29 @@ def validate_parent(self, value): return value +class ProjectCategoryLightSerializer(ProjectCategorySerializer): + class Meta(ProjectCategorySerializer.Meta): + fields = [ + "id", + "slug", + "name", + "background_color", + "foreground_color", + "organization", + "is_reviewable", + "is_followed", + ] + + +class ProjectCategorySuperLightSerializer(ProjectCategorySerializer): + class Meta(ProjectCategorySerializer.Meta): + fields = [ + "id", + "slug", + "name", + ] + + class CategoryFollowSerializer(serializers.ModelSerializer): category = ProjectCategoryLightSerializer(read_only=True) category_id = serializers.PrimaryKeyRelatedField( @@ -817,3 +666,136 @@ class Meta: model = CategoryFollow read_only_fields = ["id", "category"] fields = read_only_fields + ["category_id"] + + +@auto_translated +class TemplateSerializer( + StringsImagesSerializer, + OrganizationRelatedSerializer, + serializers.ModelSerializer, +): + string_images_fields: list[str] = [ + "description", + "project_description", + "blogentry_content", + "comment_content", + ] + string_images_forbid_fields: list[str] = [ + "name", + "project_title", + "project_purpose", + "goal_title", + "goal_description", + "review_title", + "review_description", + ] + string_images_upload_to: str = "template/images/" + string_images_view: str = "Template-images-detail" + + project_tags = TagRelatedField(many=True, required=False) + organization = SlugRelatedField(read_only=True, slug_field="code") + categories = ProjectCategoryLightSerializer(many=True, read_only=True) + # write-only + categories_ids = serializers.PrimaryKeyRelatedField( + many=True, + required=False, + write_only=True, + queryset=ProjectCategory.objects.all(), + source="categories", + ) + tabs = TemplateTabSerializer(many=True) + + class Meta: + model = Template + read_only_fields = ["id", "organization", "categories"] + fields = read_only_fields + [ + "name", + "description", + "language", + "project_title", + "project_description", + "project_purpose", + "project_tags", + "blogentry_title", + "blogentry_content", + "goal_title", + "goal_description", + "review_title", + "review_description", + "comment_content", + "categories_ids", + "tabs", + "enable_tab", + ] + + def get_related_organizations(self) -> list[Organization]: + """Retrieve the related organizations""" + return [self.validated_data.get("organization", [])] + + def get_string_images_kwargs( + self, instance: Template, field_name: str, *args: Any, **kwargs: Any + ) -> dict[str, Any]: + """Get additional kwargs for image processing based on the instance.""" + return { + "organization_code": instance.organization.code, + "template_id": instance.id, + } + + @transaction.atomic + def update(self, instance, validated_data): + tabs_data = validated_data.pop("tabs", None) + + # Update du Template + instance = super().update(instance, validated_data) + + if tabs_data is None: + return instance + + existing_tabs = {tab.id: tab for tab in instance.tabs.all()} + received_ids = set() + + for tab_data in tabs_data: + tab_id = tab_data.pop("id", None) + tab_data["template"] = instance + + tab = existing_tabs.get(tab_id, TemplateTab(**tab_data)) + + for field, value in tab_data.items(): + setattr(tab, field, value) + + tab.save() + received_ids.add(tab.id) + + to_delete = [tab_id for tab_id in existing_tabs if tab_id not in received_ids] + TemplateTab.objects.filter(id__in=to_delete).delete() + + return instance + + @transaction.atomic + def create(self, validated_data): + tabs_data = validated_data.pop("tabs", []) + + template = super().create(validated_data) + + for tab_data in tabs_data: + tab_data["template"] = template + tab = TemplateTab(**tab_data) + tab.save() + + return template + + +class ProjectTemplateSerializer(TemplateSerializer): + class Meta(TemplateSerializer.Meta): + # remove unused field + read_only_fields = [ + field + for field in TemplateSerializer.Meta.fields + if field + not in ( + "categories_ids", + "organization", + "categories", + ) + ] + fields = read_only_fields diff --git a/apps/organizations/views.py b/apps/organizations/views.py index 44d38da55..8b5291f1d 100644 --- a/apps/organizations/views.py +++ b/apps/organizations/views.py @@ -16,7 +16,7 @@ from rest_framework.response import Response from rest_framework.views import APIView -from apps.accounts.models import PeopleGroup, PeopleGroupLocation, ProjectUser +from apps.accounts.models import PeopleGroup, PeopleGroupLocation from apps.accounts.permissions import HasBasePermission from apps.accounts.serializers import ( PeopleGroupHierarchySerializer, @@ -29,6 +29,8 @@ CreateListDestroyViewSet, MultipleIDViewsetMixin, NestedOrganizationViewMixins, + NestedUserViewMixins, + QuerySerializersMixin, ) from apps.files.models import Image from apps.files.views import ImageStorageView @@ -65,30 +67,27 @@ ProjectCategoryHierarchySerializer, ProjectCategoryLightSerializer, ProjectCategorySerializer, + ProjectCategorySuperLightSerializer, TemplateSerializer, TermsAndConditionsSerializer, ) -class ProjectCategoryViewSet(MultipleIDViewsetMixin, viewsets.ModelViewSet): +class ProjectCategoryViewSet( + NestedOrganizationViewMixins, + MultipleIDViewsetMixin, + QuerySerializersMixin, + viewsets.ModelViewSet, +): serializer_class = ProjectCategorySerializer filterset_class = ProjectCategoryFilter lookup_field = "id" lookup_value_regex = "[^/]+" multiple_lookup_fields = [(ProjectCategory, "id")] - - def get_queryset(self): - if "organization_code" in self.kwargs: - return ( - ProjectCategory.objects.filter( - is_root=False, - organization__code=self.kwargs["organization_code"], - ) - .select_related("organization") - .prefetch_related("tags") - .distinct() - ) - return ProjectCategory.objects.none() + query_serializers = { + "light": ProjectCategoryLightSerializer, + "superlight": ProjectCategorySuperLightSerializer, + } def get_permissions(self): codename = map_action_to_permission(self.action, "projectcategory") @@ -101,11 +100,19 @@ def get_permissions(self): ] return super().get_permissions() - def perform_create(self, serializer): - organization = get_object_or_404( - Organization, code=self.kwargs["organization_code"] + def get_queryset(self): + return ( + ProjectCategory.objects.filter( + is_root=False, + organization__code=self.kwargs["organization_code"], + ) + .select_related("organization") + .prefetch_related("tags") + .distinct() ) - serializer.save(organization=organization) + + def perform_create(self, serializer): + serializer.save(organization=self.organization) @action( detail=True, @@ -184,12 +191,11 @@ def projects_locked_status(self, request, *args, **kwargs): return Response(status=status.HTTP_200_OK) -class CategoryFollowViewset(MultipleIDViewsetMixin, CreateListDestroyViewSet): +class CategoryFollowViewset(NestedUserViewMixins, CreateListDestroyViewSet): serializer_class = CategoryFollowSerializer filter_backends = [DjangoFilterBackend] lookup_field = "id" lookup_value_regex = "[0-9]+" - multiple_lookup_fields = [(ProjectUser, "user_id")] def get_permissions(self): codename = map_action_to_permission(self.action, "categoryfollow") @@ -202,15 +208,11 @@ def get_permissions(self): ] return super().get_permissions() - def get_queryset(self) -> QuerySet: - return self.request.user.get_user_related_queryset( - CategoryFollow.objects.filter(follower__id=self.kwargs.get("user_id")), - user_related_name="follower", - ) + def get_queryset(self) -> QuerySet[CategoryFollow]: + return self.user.modules_by_user(self.request.user).follows_categories() def perform_create(self, serializer: CategoryFollowSerializer): - follower = get_object_or_404(ProjectUser, id=self.kwargs["user_id"]) - serializer.save(follower=follower) + serializer.save(follower=self.user) class TemplateViewSet( diff --git a/services/translator/interface.py b/services/translator/interface.py index 758ca911a..c3e532450 100644 --- a/services/translator/interface.py +++ b/services/translator/interface.py @@ -1,5 +1,6 @@ from azure.ai.translation.text import TextTranslationClient -from azure.ai.translation.text.models import TranslateInputItem, TranslationTarget + +# from azure.ai.translation.text.models import TranslateInputItem, TranslationTarget from azure.core.credentials import AzureKeyCredential from django.conf import settings diff --git a/services/translator/testcases.py b/services/translator/testcases.py index a1e497427..10d263d1c 100644 --- a/services/translator/testcases.py +++ b/services/translator/testcases.py @@ -1,8 +1,7 @@ from types import SimpleNamespace from unittest.mock import _Call, call -from azure.ai.translation.text.models import TranslateInputItem, TranslationTarget - +# from azure.ai.translation.text.models import TranslateInputItem, TranslationTarget from apps.commons.test import JwtAPITestCase From 3cc61322bde340da1e96112d6b32bc3222294d71 Mon Sep 17 00:00:00 2001 From: rgermain Date: Mon, 7 Sep 2026 10:03:03 +0200 Subject: [PATCH 03/25] linter --- services/translator/interface.py | 3 +-- services/translator/testcases.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/translator/interface.py b/services/translator/interface.py index c3e532450..758ca911a 100644 --- a/services/translator/interface.py +++ b/services/translator/interface.py @@ -1,6 +1,5 @@ from azure.ai.translation.text import TextTranslationClient - -# from azure.ai.translation.text.models import TranslateInputItem, TranslationTarget +from azure.ai.translation.text.models import TranslateInputItem, TranslationTarget from azure.core.credentials import AzureKeyCredential from django.conf import settings diff --git a/services/translator/testcases.py b/services/translator/testcases.py index 10d263d1c..a1e497427 100644 --- a/services/translator/testcases.py +++ b/services/translator/testcases.py @@ -1,7 +1,8 @@ from types import SimpleNamespace from unittest.mock import _Call, call -# from azure.ai.translation.text.models import TranslateInputItem, TranslationTarget +from azure.ai.translation.text.models import TranslateInputItem, TranslationTarget + from apps.commons.test import JwtAPITestCase From dab65386b5eeb0f7ea4bf6312d94694367b62cb5 Mon Sep 17 00:00:00 2001 From: rgermain Date: Mon, 7 Sep 2026 10:12:19 +0200 Subject: [PATCH 04/25] fix: missing mirations --- .../0006_alter_projectuser_managers.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 apps/accounts/migrations/0006_alter_projectuser_managers.py diff --git a/apps/accounts/migrations/0006_alter_projectuser_managers.py b/apps/accounts/migrations/0006_alter_projectuser_managers.py new file mode 100644 index 000000000..f027097c8 --- /dev/null +++ b/apps/accounts/migrations/0006_alter_projectuser_managers.py @@ -0,0 +1,17 @@ +# Generated by Django 6.0.5 on 2026-09-07 08:11 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0005_alter_peoplegrouplocation_type"), + ] + + operations = [ + migrations.AlterModelManagers( + name="projectuser", + managers=[], + ), + ] From 1f3111344ffc12a32527712e6a76f66d9299f06b Mon Sep 17 00:00:00 2001 From: rgermain Date: Mon, 7 Sep 2026 13:28:23 +0200 Subject: [PATCH 05/25] test: re-adapt error --- .../views/test_privacy_settings_fields.py | 16 ++++++---- .../views/test_user_publication_status.py | 2 +- apps/commons/tests/test_multiple_lookups.py | 32 ++++++++++++++++--- ...t_pending_access_requests_notifications.py | 5 +-- .../tests/views/test_notification_settings.py | 16 ++++++++-- .../tests/views/test_notifications.py | 7 ++-- 6 files changed, 61 insertions(+), 17 deletions(-) diff --git a/apps/accounts/tests/views/test_privacy_settings_fields.py b/apps/accounts/tests/views/test_privacy_settings_fields.py index 8e8d67528..8baee091b 100644 --- a/apps/accounts/tests/views/test_privacy_settings_fields.py +++ b/apps/accounts/tests/views/test_privacy_settings_fields.py @@ -32,7 +32,7 @@ def set_user_privacy_settings(user, privacy): user.privacy_settings.email = privacy user.privacy_settings.save() - def assert_fields_visible(self, user, data): + def assert_fields_visible(self, user, data, skills): self.assertEqual(data["facebook"], user.facebook) self.assertEqual(data["twitter"], user.twitter) self.assertEqual(data["skype"], user.skype) @@ -45,11 +45,11 @@ def assert_fields_visible(self, user, data): self.assertEqual(data["website"], user.website) self.assertEqual(data["profile_picture"]["id"], user.profile_picture.id) self.assertEqual( - {skill["id"] for skill in data["skills"]}, + {skill["id"] for skill in skills}, {skill.id for skill in user.skills.all()}, ) - def assert_fields_hidden(self, data): + def assert_fields_hidden(self, data, skills): self.assertIsNone(data["facebook"]) self.assertIsNone(data["twitter"]) self.assertIsNone(data["skype"]) @@ -61,7 +61,7 @@ def assert_fields_hidden(self, data): self.assertIsNone(data["medium"]) self.assertIsNone(data["website"]) self.assertIsNone(data["profile_picture"]) - self.assertEqual(data["skills"], []) + self.assertEqual(skills, []) @parameterized.expand( [ @@ -108,10 +108,14 @@ def test_view_fields_retrieve_user( self.client.force_authenticate(user) response = self.client.get(reverse("ProjectUser-detail", args=(instance.id,))) self.assertEqual(response.status_code, status.HTTP_200_OK) + + skills = self.client.get(reverse("Skill-list", args=(instance.id,))).json()[ + "results" + ] if fields_visible: - self.assert_fields_visible(instance, response.data) + self.assert_fields_visible(instance, response.data, skills) else: - self.assert_fields_hidden(response.data) + self.assert_fields_hidden(response.data, skills) @parameterized.expand( [ diff --git a/apps/accounts/tests/views/test_user_publication_status.py b/apps/accounts/tests/views/test_user_publication_status.py index b111124bc..ab0f69e8a 100644 --- a/apps/accounts/tests/views/test_user_publication_status.py +++ b/apps/accounts/tests/views/test_user_publication_status.py @@ -347,7 +347,7 @@ def test_view_users_in_notifications(self, role, expected_users): for user_type in self.users } response = self.client.get( - reverse("Notification-list", args=(organization.code,)) + reverse("Notification-list", args=(organization.code, user.pk)) ) self.assertEqual(response.status_code, status.HTTP_200_OK) content = response.json()["results"] diff --git a/apps/commons/tests/test_multiple_lookups.py b/apps/commons/tests/test_multiple_lookups.py index 69481b5e3..1a38a34f3 100644 --- a/apps/commons/tests/test_multiple_lookups.py +++ b/apps/commons/tests/test_multiple_lookups.py @@ -229,25 +229,49 @@ def test_user_privacy_settings_multiple_lookups(self): def test_user_notification_settings_multiple_lookups(self): self.client.force_authenticate(self.superadmin) response = self.client.get( - reverse("NotificationSettings-detail", args=(self.user.id,)) + reverse( + "NotificationSettings-list", + args=( + self.organization.code, + self.user.id, + ), + ) ) self.assertEqual(response.status_code, status.HTTP_200_OK) content = response.json() self.assertEqual(content["id"], self.user.notification_settings.id) response = self.client.get( - reverse("NotificationSettings-detail", args=(self.user.slug,)) + reverse( + "NotificationSettings-list", + args=( + self.organization.code, + self.user.slug, + ), + ) ) self.assertEqual(response.status_code, status.HTTP_200_OK) content = response.json() self.assertEqual(content["id"], self.user.notification_settings.id) response = self.client.get( - reverse("NotificationSettings-detail", args=(self.user.keycloak_id,)) + reverse( + "NotificationSettings-list", + args=( + self.organization.code, + self.user.keycloak_id, + ), + ) ) self.assertEqual(response.status_code, status.HTTP_200_OK) content = response.json() self.assertEqual(content["id"], self.user.notification_settings.id) response = self.client.get( - reverse("NotificationSettings-detail", args=(self.outdated_user_slug,)) + reverse( + "NotificationSettings-list", + args=( + self.organization.code, + self.outdated_user_slug, + ), + ) ) self.assertEqual(response.status_code, status.HTTP_200_OK) content = response.json() diff --git a/apps/notifications/tests/tasks/test_pending_access_requests_notifications.py b/apps/notifications/tests/tasks/test_pending_access_requests_notifications.py index a4354a6b1..39d55f438 100644 --- a/apps/notifications/tests/tasks/test_pending_access_requests_notifications.py +++ b/apps/notifications/tests/tasks/test_pending_access_requests_notifications.py @@ -44,9 +44,10 @@ def test_notification_task(self): self.assertFalse(notification.is_viewed) self.assertFalse(notification.to_send) - self.client.force_authenticate(self.admins[0]) + user = self.admins[0] + self.client.force_authenticate(user) response = self.client.get( - reverse("Notification-list", args=(self.organization.code,)) + reverse("Notification-list", args=(self.organization.code, user.pk)) ) results = response.json()["results"] self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/apps/notifications/tests/views/test_notification_settings.py b/apps/notifications/tests/views/test_notification_settings.py index a24a27981..763e9949f 100644 --- a/apps/notifications/tests/views/test_notification_settings.py +++ b/apps/notifications/tests/views/test_notification_settings.py @@ -57,7 +57,13 @@ def test_retrieve_notification_settings( self.client.force_authenticate(user) for publication_status, user in self.users.items(): response = self.client.get( - reverse("NotificationSettings-detail", args=(user.id,)) + reverse( + "NotificationSettings-list", + args=( + self.organization.code, + user.id, + ), + ) ) if publication_status in retrieved_notification_settings: self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -109,7 +115,13 @@ def test_update_notification_settings(self, role, expected_code): "new_instruction": faker.boolean(), } response = self.client.patch( - reverse("NotificationSettings-detail", args=(self.user.id,)), + reverse( + "NotificationSettings-list", + args=( + self.organization.code, + self.user.id, + ), + ), data=payload, ) self.assertEqual(response.status_code, expected_code) diff --git a/apps/notifications/tests/views/test_notifications.py b/apps/notifications/tests/views/test_notifications.py index df5369f4a..25ae54c16 100644 --- a/apps/notifications/tests/views/test_notifications.py +++ b/apps/notifications/tests/views/test_notifications.py @@ -21,7 +21,10 @@ def test_list(self): ) self.client.force_authenticate(notification.receiver) response = self.client.get( - reverse("Notification-list", args=(self.organization.code,)) + reverse( + "Notification-list", + args=(self.organization.code, notification.receiver.pk), + ) ) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -49,7 +52,7 @@ def test_status_change(self): ] self.client.force_authenticate(user) response = self.client.get( - reverse("Notification-list", args=(self.organization.code,)) + reverse("Notification-list", args=(self.organization.code, user.pk)) ) self.assertEqual(response.status_code, status.HTTP_200_OK) for notification in notifications: From b12b11916b55e98b3069de3126eb1d274f10a87a Mon Sep 17 00:00:00 2001 From: rgermain Date: Mon, 7 Sep 2026 15:13:51 +0200 Subject: [PATCH 06/25] test: error user --- apps/accounts/tests/views/test_user.py | 11 ++++----- apps/accounts/views.py | 31 +++++++++++++------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/apps/accounts/tests/views/test_user.py b/apps/accounts/tests/views/test_user.py index f564c3360..b4fabc895 100644 --- a/apps/accounts/tests/views/test_user.py +++ b/apps/accounts/tests/views/test_user.py @@ -878,7 +878,7 @@ def test_notifications_count(self): self.client.force_authenticate(user) response = self.client.get(reverse("ProjectUser-detail", args=(user.id,))) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()["notifications"], 5) + self.assertEqual(response.json()["modules"]["notifications"], 5) @patch("services.keycloak.interface.KeycloakService.send_email") def test_language_from_organization(self, mocked): @@ -992,13 +992,14 @@ def test_get_people_groups(self): ] ) response = self.client.get( - reverse("ProjectUser-detail", args=(user.id,)) + reverse("ProjectUser-groups", args=(user.id,)) + f"?current_org_pk={organization.pk}" ) self.assertEqual(response.status_code, status.HTTP_200_OK) - content = response.json() - self.assertEqual(len(content["people_groups"]), 1) - self.assertEqual(content["people_groups"][0]["id"], people_group.id) + contents = response.json()["results"] + + self.assertEqual(len(contents), 1) + self.assertEqual(contents[0]["id"], people_group.id) def test_check_permissions(self): user = UserFactory() diff --git a/apps/accounts/views.py b/apps/accounts/views.py index f48afe4c5..cd4ed123c 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -2,7 +2,7 @@ from django.conf import settings from django.db import transaction -from django.db.models import Case, Prefetch, Q, QuerySet, Value, When +from django.db.models import Case, Q, QuerySet, Value, When from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404, render from django.utils import translation @@ -58,7 +58,6 @@ from apps.organizations.permissions import HasOrganizationPermission from apps.organizations.serializers import ProjectCategoryLightSerializer from apps.projects.serializers import LocationSerializer, ProjectLightSerializer -from apps.skills.models import Skill from services.google.models import GoogleAccount, GoogleGroup from services.google.tasks import ( create_google_account, @@ -151,6 +150,14 @@ def get_permissions(self): ] return super().get_permissions() + @cached_property + def organization(self): + return get_object_or_404( + Organization.objects.filter( + pk=self.request.query_params.get("current_org_pk") + ) + ) + def annotate_organization_role( self, queryset: QuerySet, organization: Organization ) -> QuerySet: @@ -194,16 +201,12 @@ def get_queryset(self): queryset = self.request.user.get_user_queryset() organization_pk = self.request.query_params.get("current_org_pk") if organization_pk is not None: - organization = Organization.objects.get(pk=organization_pk) - queryset = self.annotate_organization_role(queryset, organization) + queryset = self.annotate_organization_role(queryset, self.organization) + if self.action == "admin_list": queryset = self.annotate_keycloak_email_verified(queryset) - skills_prefetch = Prefetch( - "skills", queryset=Skill.objects.select_related("tag") - ) - return queryset.prefetch_related(skills_prefetch, "groups").select_related( - "researcher" - ) + + return queryset.select_related("researcher") def get_object(self): """ @@ -233,11 +236,7 @@ def get_serializer_class(self): def get_serializer_context(self): context = super().get_serializer_context() - context.update({"request": self.request}) - current_org_pk = self.request.query_params.get("current_org_pk") - if current_org_pk: - organization = get_object_or_404(Organization, pk=current_org_pk) - context.update({"organization": organization}) + context.update({"request": self.request, "organization": self.organization}) return context @extend_schema( @@ -373,7 +372,7 @@ def has_permissions(self, request, *args, **kwargs): ) def groups(self, request, *args, **kwargs): user = self.get_object() - queryset = user.modules_by_user(request.user).groups() + queryset = user.modules_by_user(request.user, self.organization).groups() page = self.paginate_queryset(queryset) if page is not None: From cf11f38a9c8cd75225c34619b005020aa522ae42 Mon Sep 17 00:00:00 2001 From: rgermain Date: Tue, 8 Sep 2026 09:53:48 +0200 Subject: [PATCH 07/25] fix: cached proerty --- apps/accounts/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index cd4ed123c..c76d7002a 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -1,4 +1,5 @@ import uuid +from functools import cached_property from django.conf import settings from django.db import transaction From 14e56ad918a77ba84b08996f4607bbd9d2e3cb57 Mon Sep 17 00:00:00 2001 From: rgermain Date: Tue, 8 Sep 2026 14:26:13 +0200 Subject: [PATCH 08/25] fix: organizations --- apps/accounts/views.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index c76d7002a..1b770fa90 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -153,11 +153,11 @@ def get_permissions(self): @cached_property def organization(self): - return get_object_or_404( - Organization.objects.filter( - pk=self.request.query_params.get("current_org_pk") - ) - ) + current_org_pk = self.request.query_params.get("current_org_pk") + if not current_org_pk: + return None + + return get_object_or_404(Organization.objects.filter(pk=current_org_pk)) def annotate_organization_role( self, queryset: QuerySet, organization: Organization @@ -200,8 +200,7 @@ def annotate_keycloak_email_verified(self, queryset: QuerySet) -> QuerySet: def get_queryset(self): queryset = self.request.user.get_user_queryset() - organization_pk = self.request.query_params.get("current_org_pk") - if organization_pk is not None: + if self.organization is not None: queryset = self.annotate_organization_role(queryset, self.organization) if self.action == "admin_list": @@ -259,10 +258,8 @@ def get_serializer_context(self): ) def get_by_email(self, request, *args, **kwargs): queryset = ProjectUser.objects.all() - current_org_pk = request.query_params.get("current_org_pk") - if current_org_pk is not None: - organization = Organization.objects.get(pk=current_org_pk) - queryset = self.annotate_organization_role(queryset, organization) + if self.organization is not None: + queryset = self.annotate_organization_role(queryset, self.organization) user = queryset.filter( Q(email=kwargs.get("email")) | Q(personal_email=kwargs.get("email")) ).distinct() From dfb54761603fbb409708fa2a2087cc6e807831ea Mon Sep 17 00:00:00 2001 From: rgermain Date: Tue, 8 Sep 2026 16:08:59 +0200 Subject: [PATCH 09/25] fix tests --- apps/accounts/views.py | 9 ++++----- apps/commons/tests/test_multiple_lookups.py | 12 +++++++----- apps/notifications/tests/views/test_notifications.py | 5 ++++- .../tests/views/test_category_follow.py | 6 +++--- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 1b770fa90..f01c6e95c 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -1099,17 +1099,16 @@ class UserProfilePictureView(NestedUserViewMixins, ImageStorageView): ] def get_queryset(self): - return self.user.images.all() + return Image.objects.filter(user=self.user) @staticmethod def upload_to(instance, filename) -> str: return f"account/profile/{uuid.uuid4()}#{instance.name}" def add_image_to_model(self, image): - user = ProjectUser.objects.get(id=self.kwargs["user_id"]) - user.profile_picture = image - user.save() - image.owner = user + self.user.profile_picture = image + self.user.save() + image.owner = self.user image.save() return f"/v1/user/{self.kwargs['user_id']}/profile-picture/{image.id}" diff --git a/apps/commons/tests/test_multiple_lookups.py b/apps/commons/tests/test_multiple_lookups.py index 1a38a34f3..36ae80432 100644 --- a/apps/commons/tests/test_multiple_lookups.py +++ b/apps/commons/tests/test_multiple_lookups.py @@ -35,16 +35,18 @@ def setUpTestData(cls): super().setUpTestData() cls.superadmin = UserFactory(groups=[get_superadmins_group()]) - cls.user = UserFactory(profile_picture=cls.get_test_image()) - cls.outdated_user_slug = faker.word() - cls.user.outdated_slugs = [cls.outdated_user_slug] - cls.user.save() - cls.organization = OrganizationFactory() cls.outdated_organization_slug = faker.word() cls.organization.outdated_slugs = [cls.outdated_organization_slug] cls.organization.save() + cls.user = UserFactory( + profile_picture=cls.get_test_image(), groups=[cls.organization.get_users()] + ) + cls.outdated_user_slug = faker.word() + cls.user.outdated_slugs = [cls.outdated_user_slug] + cls.user.save() + cls.project = ProjectFactory( organizations=[cls.organization], header_image=cls.get_test_image() ) diff --git a/apps/notifications/tests/views/test_notifications.py b/apps/notifications/tests/views/test_notifications.py index 25ae54c16..aa8da99ea 100644 --- a/apps/notifications/tests/views/test_notifications.py +++ b/apps/notifications/tests/views/test_notifications.py @@ -19,6 +19,9 @@ def test_list(self): notification = NotificationFactory( project=self.project, organization=self.organization ) + + notification.receiver.groups.add(self.organization.get_users()) + self.client.force_authenticate(notification.receiver) response = self.client.get( reverse( @@ -29,7 +32,7 @@ def test_list(self): self.assertEqual(response.status_code, status.HTTP_200_OK) def test_status_change(self): - user = UserFactory() + user = UserFactory(groups=[self.organization.get_users()]) notifications = NotificationFactory.create_batch( 2, receiver=user, diff --git a/apps/organizations/tests/views/test_category_follow.py b/apps/organizations/tests/views/test_category_follow.py index 458dbdc23..930ebad09 100644 --- a/apps/organizations/tests/views/test_category_follow.py +++ b/apps/organizations/tests/views/test_category_follow.py @@ -120,13 +120,13 @@ def test_list_category_follows(self, role, retrieved_follows): self.client.force_authenticate(user) for publication_status, user in self.users.items(): response = self.client.get(reverse("CategoryFollow-list", args=(user.id,))) - self.assertEqual(response.status_code, status.HTTP_200_OK) - content = response.json()["results"] if publication_status in retrieved_follows: + self.assertEqual(response.status_code, status.HTTP_200_OK) + content = response.json()["results"] self.assertEqual(len(content), 1) self.assertEqual( content[0]["id"], self.category_follows[publication_status].id, ) else: - self.assertEqual(len(content), 0) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) From b025fdcb751093c2e93f512cb7a27d7043f3e0a6 Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 9 Sep 2026 11:31:50 +0200 Subject: [PATCH 10/25] fix tests --- .../views/test_user_publication_status.py | 10 ++----- apps/notifications/views.py | 12 +++++++- apps/skills/serializers.py | 26 +++++++++++++++++ .../tests/views/test_user_mentorship.py | 21 +++++++------- apps/skills/views.py | 29 ++++++------------- 5 files changed, 60 insertions(+), 38 deletions(-) diff --git a/apps/accounts/tests/views/test_user_publication_status.py b/apps/accounts/tests/views/test_user_publication_status.py index ab0f69e8a..6ccc0bc1b 100644 --- a/apps/accounts/tests/views/test_user_publication_status.py +++ b/apps/accounts/tests/views/test_user_publication_status.py @@ -325,7 +325,6 @@ def test_view_users_in_invitations(self, role, expected_users): @parameterized.expand( [ - (TestRoles.DEFAULT, ("public", None, None)), (TestRoles.SUPERADMIN, ("public", "private", "org")), (TestRoles.ORG_ADMIN, ("public", "private", "org")), (TestRoles.ORG_FACILITATOR, ("public", "private", "org")), @@ -351,19 +350,16 @@ def test_view_users_in_notifications(self, role, expected_users): ) self.assertEqual(response.status_code, status.HTTP_200_OK) content = response.json()["results"] - self.assertEqual(len(content), len(expected_users)) + self.assertEqual( { (notification["sender"]["id"], notification["id"]) for notification in content }, { - ( - (self.users[user_type].id, notifications[user_type].id) - if user_type in expected_users - else (None, notifications[user_type].id) - ) + ((self.users[user_type].id, notifications[user_type].id)) for user_type in self.users + if user_type in expected_users }, ) diff --git a/apps/notifications/views.py b/apps/notifications/views.py index 58ffb3153..0d363d218 100644 --- a/apps/notifications/views.py +++ b/apps/notifications/views.py @@ -14,6 +14,7 @@ from apps.accounts.permissions import HasBasePermission from apps.commons.permissions import IsOwner, ReadOnly +from apps.commons.serializers import RetrieveUpdateModelViewSet from apps.commons.views import ( ListViewSet, NestedOrganizationUserViewMixins, @@ -21,6 +22,7 @@ from apps.emailing.tasks import send_email_task from apps.emailing.utils import render_message from apps.notifications.filters import NotificationFilter +from apps.notifications.models import NotificationSettings from apps.organizations.models import Organization from apps.organizations.permissions import HasOrganizationPermission @@ -59,7 +61,9 @@ def list(self, request, *args, **kwargs): return response -class NotificationSettingsViewSet(NestedOrganizationUserViewMixins, viewsets.ViewSet): +class NotificationSettingsViewSet( + NestedOrganizationUserViewMixins, RetrieveUpdateModelViewSet +): """Allows getting or modifying a user's notification settings.""" serializer_class = NotificationSettingsSerializer @@ -71,14 +75,20 @@ class NotificationSettingsViewSet(NestedOrganizationUserViewMixins, viewsets.Vie | HasOrganizationPermission("change_projectuser"), ] + # queryset user only for check object permissions + def get_queryset(self): + return NotificationSettings.objects.filter(user=self.user) + def list(self, request, *args, **kwargs): instance = self.user.notification_settings + self.check_object_permissions(request, instance) serializer = self.serializer_class(instance) return Response(serializer.data) def patch(self, request, *args, **kwargs): instance = self.user.notification_settings + self.check_object_permissions(request, instance) serializer = self.serializer_class( instance, diff --git a/apps/skills/serializers.py b/apps/skills/serializers.py index 2593906e6..e6f90ed0a 100644 --- a/apps/skills/serializers.py +++ b/apps/skills/serializers.py @@ -6,6 +6,7 @@ from rest_framework import serializers from rest_framework.fields import empty +from apps.accounts.models import ProjectUser from apps.commons.fields import ( HiddenPrimaryKeyRelatedField, UserMultipleIdRelatedField, @@ -320,3 +321,28 @@ class Meta: "created_at", ] fields = read_only_fields + + +class UserSkillLightSerializer(serializers.Serializer): + user = serializers.SerializerMethodField() + can_mentor_on = serializers.SerializerMethodField() + needs_mentor_on = serializers.SerializerMethodField() + + class Meta: + read_only_fields = ["user", "can_mentor_on", "needs_mentor_on"] + fields = read_only_fields + + def get_user(self, instance: ProjectUser): + from apps.accounts.serializers import UserLighterSerializer + + return UserLighterSerializer(instance).data + + def get_can_mentor_on(self, instance: ProjectUser): + if hasattr(instance, "can_mentor_on"): + return SkillLightSerializer(instance.can_mentor_on, many=True).data + return None + + def get_needs_mentor_on(self, instance: ProjectUser): + if hasattr(instance, "needs_mentor_on"): + return SkillLightSerializer(instance.needs_mentor_on, many=True).data + return None diff --git a/apps/skills/tests/views/test_user_mentorship.py b/apps/skills/tests/views/test_user_mentorship.py index 80596812e..e745a220d 100644 --- a/apps/skills/tests/views/test_user_mentorship.py +++ b/apps/skills/tests/views/test_user_mentorship.py @@ -192,15 +192,16 @@ def test_retrieve_mentor_candidates(self, role, mentors): ) ) self.assertEqual(response.status_code, status.HTTP_200_OK) - content = response.json()["results"] - self.assertEqual(len(content), len(mentors)) + contents = response.json()["results"] + self.assertEqual(len(contents), len(mentors)) + print(contents) self.assertSetEqual( - {user["id"] for user in content}, + {content["user"]["id"] for content in contents}, {self.users[mentor].id for mentor in mentors}, ) - for user in content: + for content in contents: self.assertSetEqual( - {skill["tag"]["id"] for skill in user["can_mentor_on"]}, + {skill["tag"]["id"] for skill in content["can_mentor_on"]}, {self.mentor_skill_1.id, self.mentor_skill_2.id}, ) @@ -267,14 +268,14 @@ def test_retrieve_mentoree_candidates(self, role, mentorees): ) ) self.assertEqual(response.status_code, status.HTTP_200_OK) - content = response.json()["results"] - self.assertEqual(len(content), len(mentorees)) + contents = response.json()["results"] + self.assertEqual(len(contents), len(mentorees)) self.assertSetEqual( - {user["id"] for user in content}, + {content["user"]["id"] for content in contents}, {self.users[mentoree].id for mentoree in mentorees}, ) - for user in content: + for content in contents: self.assertSetEqual( - {skill["tag"]["id"] for skill in user["needs_mentor_on"]}, + {skill["tag"]["id"] for skill in content["needs_mentor_on"]}, {self.mentoree_skill_1.id, self.mentoree_skill_2.id}, ) diff --git a/apps/skills/views.py b/apps/skills/views.py index f26d7ea13..385bff6e7 100644 --- a/apps/skills/views.py +++ b/apps/skills/views.py @@ -18,11 +18,11 @@ from apps.accounts.models import PrivacySettings, ProjectUser from apps.accounts.permissions import HasBasePermission -from apps.accounts.serializers import UserLightSerializer from apps.commons.permissions import IsOwner, ReadOnly, WillBeOwner from apps.commons.utils import map_action_to_permission from apps.commons.views import ( MultipleIDViewsetMixin, + NestedOrganizationUserViewMixins, NestedUserViewMixins, PaginatedViewSet, ReadDestroyModelViewSet, @@ -52,6 +52,7 @@ TagClassificationRemoveTagsSerializer, TagClassificationSerializer, TagSerializer, + UserSkillLightSerializer, ) from .utils import ( set_default_language_title_and_description, @@ -502,35 +503,23 @@ def mentoree_skill(self, request, *args, **kwargs): return self.get_paginated_list(tags) -class UserMentorshipViewset(MultipleIDViewsetMixin, PaginatedViewSet): - serializer_class = UserLightSerializer +class UserMentorshipViewset(NestedOrganizationUserViewMixins, PaginatedViewSet): + serializer_class = UserSkillLightSerializer permission_classes = [ReadOnly] - multiple_lookup_fields = [(ProjectUser, "user_id")] - - def get_organization(self): - organization_code = self.kwargs["organization_code"] - return get_object_or_404(Organization, code=organization_code) - - def get_user(self): - organization = self.get_organization() - user_id = self.kwargs["user_id"] - return get_object_or_404(organization.get_all_members(), id=user_id) def get_user_queryset(self): - organization = self.get_organization() request_user = self.request.user - organization_menbers_id: list[int] = organization.get_all_members().values_list( - "id", flat=True - ) user_queryset = self.request.user.get_user_queryset().filter( - id__in=organization_menbers_id + groups__organizations=self.organization ) + if request_user.is_authenticated: if request_user.is_superuser or ( - organization.admins.all() | organization.facilitators.all() + self.organization.admins.all() | self.organization.facilitators.all() ).contains(request_user): return user_queryset - if request_user.id in organization_menbers_id: + + if user_queryset.contains(request_user): return user_queryset.filter( Q( privacy_settings__skills__in=[ From e2b3cb054ea2dc51b3399933893d56b31e8e3301 Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 9 Sep 2026 11:49:57 +0200 Subject: [PATCH 11/25] fix: skills --- apps/skills/serializers.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/skills/serializers.py b/apps/skills/serializers.py index e6f90ed0a..1a01f61cc 100644 --- a/apps/skills/serializers.py +++ b/apps/skills/serializers.py @@ -335,14 +335,18 @@ class Meta: def get_user(self, instance: ProjectUser): from apps.accounts.serializers import UserLighterSerializer - return UserLighterSerializer(instance).data + return UserLighterSerializer(instance, context=self.context).data def get_can_mentor_on(self, instance: ProjectUser): if hasattr(instance, "can_mentor_on"): - return SkillLightSerializer(instance.can_mentor_on, many=True).data + can_mentor_on: list[int] = instance.can_mentor_on + skills = Skill.objects.filter(id__in=can_mentor_on) + return SkillLightSerializer(skills, many=True, context=self.context).data return None def get_needs_mentor_on(self, instance: ProjectUser): if hasattr(instance, "needs_mentor_on"): - return SkillLightSerializer(instance.needs_mentor_on, many=True).data + needs_mentor_on: list[int] = instance.needs_mentor_on + skills = Skill.objects.filter(id__in=needs_mentor_on) + return SkillLightSerializer(skills, many=True, context=self.context).data return None From 7fc27bdc41593a07111e347a743e59329da34ac9 Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 9 Sep 2026 13:52:44 +0200 Subject: [PATCH 12/25] fix: message i18n --- locale/ca/LC_MESSAGES/django.po | 8 ++++---- locale/de/LC_MESSAGES/django.po | 8 ++++---- locale/en/LC_MESSAGES/django.po | 8 ++++---- locale/es/LC_MESSAGES/django.po | 8 ++++---- locale/et/LC_MESSAGES/django.po | 8 ++++---- locale/fr/LC_MESSAGES/django.po | 8 ++++---- locale/nl/LC_MESSAGES/django.po | 8 ++++---- 7 files changed, 28 insertions(+), 28 deletions(-) diff --git a/locale/ca/LC_MESSAGES/django.po b/locale/ca/LC_MESSAGES/django.po index 0e4d44273..23e91e4c4 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-08-14 10:41+0200\n" +"POT-Creation-Date: 2026-09-09 13:51+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -176,7 +176,7 @@ msgstr "Estat de publicació desconegut" msgid "Unknown publication status '{publication_status}'" msgstr "Estat de publicació desconegut '{publication_status}'" -#: apps/commons/fields.py:31 apps/skills/serializers.py:111 +#: apps/commons/fields.py:31 apps/skills/serializers.py:112 msgid "This field is required." msgstr "Aquest camp és obligatori." @@ -185,7 +185,7 @@ msgstr "Aquest camp és obligatori." msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "Id no vàlid \"{user_id}\" - l'objecte no existeix." -#: apps/commons/fields.py:34 apps/skills/serializers.py:116 +#: apps/commons/fields.py:34 apps/skills/serializers.py:117 #, 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}." @@ -1637,7 +1637,7 @@ msgstr "El títol de l'etiqueta ha de tenir 50 caràcters o menys" msgid "Tag description must be 500 characters or less" msgstr "La descripció de l'etiqueta ha de tenir 500 caràcters o menys" -#: apps/skills/serializers.py:113 +#: apps/skills/serializers.py:114 #, python-brace-format msgid "Invalid id \"{tag_classification_id}\" - object does not exist." msgstr "Id no vàlid \"{tag_classification_id}\" - l'objecte no existeix." diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index bcd0101cb..b08c2b4de 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-08-14 10:41+0200\n" +"POT-Creation-Date: 2026-09-09 13:51+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -178,7 +178,7 @@ msgstr "Unbekannter Veröffentlichungsstatus" msgid "Unknown publication status '{publication_status}'" msgstr "Unbekannter Veröffentlichungsstatus '{publication_status}'" -#: apps/commons/fields.py:31 apps/skills/serializers.py:111 +#: apps/commons/fields.py:31 apps/skills/serializers.py:112 msgid "This field is required." msgstr "Dieses Feld ist erforderlich." @@ -187,7 +187,7 @@ msgstr "Dieses Feld ist erforderlich." msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "Ungültige ID \"{user_id}\" – Objekt existiert nicht." -#: apps/commons/fields.py:34 apps/skills/serializers.py:116 +#: apps/commons/fields.py:34 apps/skills/serializers.py:117 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "" @@ -1656,7 +1656,7 @@ msgstr "Der Tag-Titel darf maximal 50 Zeichen lang sein" msgid "Tag description must be 500 characters or less" msgstr "Die Tag-Beschreibung darf maximal 500 Zeichen lang sein" -#: apps/skills/serializers.py:113 +#: apps/skills/serializers.py:114 #, python-brace-format msgid "Invalid id \"{tag_classification_id}\" - object does not exist." msgstr "Ungültige ID \"{tag_classification_id}\" – Objekt existiert nicht." diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index 3115b88dc..71211020e 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-08-14 10:41+0200\n" +"POT-Creation-Date: 2026-09-09 13:51+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -154,7 +154,7 @@ msgstr "" msgid "Unknown publication status '{publication_status}'" msgstr "" -#: apps/commons/fields.py:31 apps/skills/serializers.py:111 +#: apps/commons/fields.py:31 apps/skills/serializers.py:112 msgid "This field is required." msgstr "" @@ -163,7 +163,7 @@ msgstr "" msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "" -#: apps/commons/fields.py:34 apps/skills/serializers.py:116 +#: apps/commons/fields.py:34 apps/skills/serializers.py:117 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "" @@ -1293,7 +1293,7 @@ msgstr "" msgid "Tag description must be 500 characters or less" msgstr "" -#: apps/skills/serializers.py:113 +#: apps/skills/serializers.py:114 #, python-brace-format msgid "Invalid id \"{tag_classification_id}\" - object does not exist." msgstr "" diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po index 1bfc2b9e5..9a34dbbd6 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-08-14 10:41+0200\n" +"POT-Creation-Date: 2026-09-09 13:51+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -176,7 +176,7 @@ msgstr "Estado de publicación desconocido" msgid "Unknown publication status '{publication_status}'" msgstr "Estado de publicación desconocido '{publication_status}'" -#: apps/commons/fields.py:31 apps/skills/serializers.py:111 +#: apps/commons/fields.py:31 apps/skills/serializers.py:112 msgid "This field is required." msgstr "Este campo es obligatorio." @@ -185,7 +185,7 @@ msgstr "Este campo es obligatorio." msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "ID no válido \"{user_id}\" - el objeto no existe." -#: apps/commons/fields.py:34 apps/skills/serializers.py:116 +#: apps/commons/fields.py:34 apps/skills/serializers.py:117 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "" @@ -1636,7 +1636,7 @@ msgstr "El título de la etiqueta debe tener 50 caracteres o menos" msgid "Tag description must be 500 characters or less" msgstr "La descripción de la etiqueta debe tener 500 caracteres o menos" -#: apps/skills/serializers.py:113 +#: apps/skills/serializers.py:114 #, python-brace-format msgid "Invalid id \"{tag_classification_id}\" - object does not exist." msgstr "ID no válida \"{tag_classification_id}\" - el objeto no existe." diff --git a/locale/et/LC_MESSAGES/django.po b/locale/et/LC_MESSAGES/django.po index e9fca42d9..7d7807d37 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-08-14 10:41+0200\n" +"POT-Creation-Date: 2026-09-09 13:51+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -175,7 +175,7 @@ msgstr "Tundmatu avaldamise olek" msgid "Unknown publication status '{publication_status}'" msgstr "Tundmatu avaldamise olek '{publication_status}'" -#: apps/commons/fields.py:31 apps/skills/serializers.py:111 +#: apps/commons/fields.py:31 apps/skills/serializers.py:112 msgid "This field is required." msgstr "See väli on kohustuslik." @@ -184,7 +184,7 @@ msgstr "See väli on kohustuslik." msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "Vigane ID \"{user_id}\" - objekti ei eksisteeri." -#: apps/commons/fields.py:34 apps/skills/serializers.py:116 +#: apps/commons/fields.py:34 apps/skills/serializers.py:117 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "Vale tüüp. Oodati stringi väärtust, saadi {data_type}." @@ -1614,7 +1614,7 @@ msgstr "Sildi pealkiri peab olema 50 tähemärki või vähem" msgid "Tag description must be 500 characters or less" msgstr "Sildi kirjeldus peab olema 500 tähemärki või vähem" -#: apps/skills/serializers.py:113 +#: apps/skills/serializers.py:114 #, python-brace-format msgid "Invalid id \"{tag_classification_id}\" - object does not exist." msgstr "Vigane ID \"{tag_classification_id}\" - objekti ei eksisteeri." diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 05f62edc8..a0064568f 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-08-14 10:41+0200\n" +"POT-Creation-Date: 2026-09-09 13:51+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -178,7 +178,7 @@ msgstr "Statut de publication inconnu" msgid "Unknown publication status '{publication_status}'" msgstr "Statut de publication inconnu '{publication_status}'" -#: apps/commons/fields.py:31 apps/skills/serializers.py:111 +#: apps/commons/fields.py:31 apps/skills/serializers.py:112 msgid "This field is required." msgstr "Ce champ est obligatoire." @@ -187,7 +187,7 @@ msgstr "Ce champ est obligatoire." msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "identifiant invalide \"{user_id}\" - cet objet n'existe pas." -#: apps/commons/fields.py:34 apps/skills/serializers.py:116 +#: apps/commons/fields.py:34 apps/skills/serializers.py:117 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "Type incorrect. Valeur str attendue, {data_type} reçue." @@ -1635,7 +1635,7 @@ msgstr "Le titre du tag doit comporter 50 caractères ou moins" msgid "Tag description must be 500 characters or less" msgstr "La description du tag doit comporter 500 caractères ou moins" -#: apps/skills/serializers.py:113 +#: apps/skills/serializers.py:114 #, python-brace-format msgid "Invalid id \"{tag_classification_id}\" - object does not exist." msgstr "" diff --git a/locale/nl/LC_MESSAGES/django.po b/locale/nl/LC_MESSAGES/django.po index e5a7b950b..4fac8cac0 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-08-14 10:41+0200\n" +"POT-Creation-Date: 2026-09-09 13:51+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -178,7 +178,7 @@ msgstr "Onbekende publicatiestatus" msgid "Unknown publication status '{publication_status}'" msgstr "Onbekende publicatiestatus '{publication_status}'" -#: apps/commons/fields.py:31 apps/skills/serializers.py:111 +#: apps/commons/fields.py:31 apps/skills/serializers.py:112 msgid "This field is required." msgstr "Dit veld is verplicht." @@ -187,7 +187,7 @@ msgstr "Dit veld is verplicht." msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "Ongeldige id \"{user_id}\" - object bestaat niet." -#: apps/commons/fields.py:34 apps/skills/serializers.py:116 +#: apps/commons/fields.py:34 apps/skills/serializers.py:117 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "Onjuist type. Verwachte stringwaarde, ontvangen {data_type}." @@ -1647,7 +1647,7 @@ msgstr "Tagtitel moet 50 tekens of minder zijn" msgid "Tag description must be 500 characters or less" msgstr "Tagbeschrijving moet 500 tekens of minder zijn" -#: apps/skills/serializers.py:113 +#: apps/skills/serializers.py:114 #, python-brace-format msgid "Invalid id \"{tag_classification_id}\" - object does not exist." msgstr "Ongeldige id \"{tag_classification_id}\" - object bestaat niet." From fab00db3ed1bee1779d91bd8218f688eb1f18d5e Mon Sep 17 00:00:00 2001 From: rgermain Date: Thu, 10 Sep 2026 11:42:16 +0200 Subject: [PATCH 13/25] fix: tests --- apps/accounts/models.py | 10 +++- apps/modules/user.py | 57 ++++++++++++++++++- .../tests/views/test_user_mentorship.py | 5 +- 3 files changed, 66 insertions(+), 6 deletions(-) diff --git a/apps/accounts/models.py b/apps/accounts/models.py index 84a7982dc..d8749d02d 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -985,9 +985,13 @@ def get_permissions_representations(self): """Return a list of the permissions representations.""" return [] + def get_organizations_queryset(self) -> QuerySet[Organization]: + """Return the organizations related to this model.""" + return Organization.objects.none() + def get_related_organizations(self) -> list["Organization"]: """Return the organizations related to this model.""" - return [] + return list(self.get_organizations_queryset()) class InvitationUser(AnonymousUser): @@ -1032,6 +1036,6 @@ def _query_function(self, queryset, *ar, **kw): get_instruction_related_queryset = _query_function get_event_related_queryset = _query_function - def get_related_organizations(self) -> list["Organization"]: + def get_organizations_queryset(self) -> QuerySet[Organization]: """Return the organizations related to this model.""" - return list(Organization.objects.all()) + return Organization.objects.all() diff --git a/apps/modules/user.py b/apps/modules/user.py index f01841455..ab5728e16 100644 --- a/apps/modules/user.py +++ b/apps/modules/user.py @@ -1,10 +1,17 @@ from functools import cached_property +from django.contrib.auth.models import Group from django.db.models import ( QuerySet, ) -from apps.accounts.models import PeopleGroup, ProjectUser +from apps.accounts.models import ( + AnonymousUser, + PeopleGroup, + PrivacySettings, + ProjectUser, +) +from apps.accounts.utils import get_superadmins_group from apps.commons.models import GroupData from apps.files.models import ProjectUserAttachmentFile, ProjectUserAttachmentLink from apps.modules.base import AbstractModules, organization_related, register_module @@ -19,8 +26,54 @@ class UserModules(AbstractModules): instance: ProjectUser + @cached_property + def _privacy_settings(self): + """generate a privacy informations (only for sklls now) to filter queryset""" + + # return privacy filde from user + privacy = self.instance.privacy_settings + + # privacy "ORGANIZATION" + in_organization = self.instance.groups.filter( + organizations__isnull=False, + organizations__in=self.user.get_organizations_queryset(), + ).exists() + + # privacy "hide" + is_connected = self.user.is_authenticated + if isinstance(self.user, AnonymousUser): + is_admin = False + else: + is_admin = self.user.groups.contains(get_superadmins_group()) or ( + Group.objects.filter( + organizations__isnull=False, + organizations__in=self.instance.get_related_organizations(), + name__contains="admins", + users=self.user, + ).exists() + ) + # is same user + is_same_user = self.user.pk == self.instance.pk + + # return boolean for each privacy field + return { + "skills": any( + ( + is_same_user, + is_connected + and is_admin + and privacy.skills == PrivacySettings.PrivacyChoices.HIDE, + in_organization + and privacy.skills == PrivacySettings.PrivacyChoices.ORGANIZATION, + ) + ) + } + def skills(self) -> QuerySet[Skill]: - return self.instance.skills.all() + qs = self.instance.skills.all() + if self._privacy_settings["skills"]: + return qs + return qs.none() @organization_related def mentor(self) -> QuerySet[Mentoring]: diff --git a/apps/skills/tests/views/test_user_mentorship.py b/apps/skills/tests/views/test_user_mentorship.py index e745a220d..f83eb0fb4 100644 --- a/apps/skills/tests/views/test_user_mentorship.py +++ b/apps/skills/tests/views/test_user_mentorship.py @@ -181,6 +181,8 @@ def setUpTestData(cls): def test_retrieve_mentor_candidates(self, role, mentors): organization = self.organization user = self.get_parameterized_test_user(role, instances=[organization]) + user.groups.add(organization.get_users()) + SkillFactory(user=user, tag=self.mentor_skill_1, needs_mentor=True) SkillFactory(user=user, tag=self.mentor_skill_2, needs_mentor=True) SkillFactory(user=user, tag=self.other_skill, needs_mentor=True) @@ -194,7 +196,6 @@ def test_retrieve_mentor_candidates(self, role, mentors): self.assertEqual(response.status_code, status.HTTP_200_OK) contents = response.json()["results"] self.assertEqual(len(contents), len(mentors)) - print(contents) self.assertSetEqual( {content["user"]["id"] for content in contents}, {self.users[mentor].id for mentor in mentors}, @@ -257,6 +258,8 @@ def test_retrieve_mentor_candidates(self, role, mentors): def test_retrieve_mentoree_candidates(self, role, mentorees): organization = self.organization user = self.get_parameterized_test_user(role, instances=[organization]) + user.groups.add(organization.get_users()) + SkillFactory(user=user, tag=self.mentoree_skill_1, can_mentor=True) SkillFactory(user=user, tag=self.mentoree_skill_2, can_mentor=True) SkillFactory(user=user, tag=self.other_skill, can_mentor=True) From 87c2677957311a981881faa8eb40f8f6aa943581 Mon Sep 17 00:00:00 2001 From: rgermain Date: Thu, 10 Sep 2026 11:58:30 +0200 Subject: [PATCH 14/25] fix: tests privacys --- apps/skills/views.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/apps/skills/views.py b/apps/skills/views.py index 385bff6e7..5ca2fe9ce 100644 --- a/apps/skills/views.py +++ b/apps/skills/views.py @@ -609,25 +609,28 @@ def mentor_candidate(self, request, *args, **kwargs): """ Get all users in current organization that have at least one skill that could be mentored by the user. """ - user = get_object_or_404( - self.request.user.get_user_queryset(), id=self.kwargs["user_id"] - ) + user_skills = self.user.modules_by_user( + request.user, self.organization + ).skills() user_mentoree_skills = Tag.objects.filter( - skills__user=user, skills__needs_mentor=True + skills__in=user_skills.filter(needs_mentor=True) ).distinct() + mentors_skills = Skill.objects.filter( - user__in=self.get_user_queryset(), - can_mentor=True, - tag__in=user_mentoree_skills, - ).distinct() - users = ProjectUser.objects.filter(skills__in=mentors_skills).annotate( - can_mentor_on=ArrayAgg( - "skills", - filter=Q( - skills__can_mentor=True, - skills__tag__in=user_mentoree_skills, - ), - distinct=True, + can_mentor=True, tag__in=user_mentoree_skills + ) + users = ( + request.user.get_user_queryset() + .filter(skills__in=mentors_skills) + .annotate( + can_mentor_on=ArrayAgg( + "skills", + filter=Q( + skills__can_mentor=True, + skills__tag__in=user_mentoree_skills, + ), + distinct=True, + ) ) ) return self.get_paginated_list(users) From 8b3c478df55c583a5b65a8e1a346e98abe1abb7a Mon Sep 17 00:00:00 2001 From: rgermain Date: Thu, 10 Sep 2026 12:32:50 +0200 Subject: [PATCH 15/25] fix: profile pictures --- apps/accounts/serializers.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/apps/accounts/serializers.py b/apps/accounts/serializers.py index 344ef1a42..9c9150dd8 100644 --- a/apps/accounts/serializers.py +++ b/apps/accounts/serializers.py @@ -149,6 +149,26 @@ class UserSerializer( read_only=True, required=False, allow_null=True ) + # Write only profile picture fields + profile_picture_file = serializers.ImageField( + write_only=True, required=False, allow_null=True + ) + profile_picture_scale_x = serializers.FloatField( + write_only=True, required=False, allow_null=True + ) + profile_picture_scale_y = serializers.FloatField( + write_only=True, required=False, allow_null=True + ) + profile_picture_left = serializers.FloatField( + write_only=True, required=False, allow_null=True + ) + profile_picture_top = serializers.FloatField( + write_only=True, required=False, allow_null=True + ) + profile_picture_natural_ratio = serializers.FloatField( + write_only=True, required=False, allow_null=True + ) + class Meta: model = ProjectUser read_only_fields = [ @@ -190,6 +210,12 @@ class Meta: "skype", "landline_phone", "twitter", + "profile_picture_file", + "profile_picture_scale_x", + "profile_picture_scale_y", + "profile_picture_left", + "profile_picture_top", + "profile_picture_natural_ratio", ] @cached_property From df0d459c2bab27f137f12439d28aad770c6c7573 Mon Sep 17 00:00:00 2001 From: rgermain Date: Thu, 10 Sep 2026 12:54:58 +0200 Subject: [PATCH 16/25] fix: skills public --- apps/modules/user.py | 5 ++ apps/organizations/serializers.py | 99 ++++++++++++------------------- 2 files changed, 44 insertions(+), 60 deletions(-) diff --git a/apps/modules/user.py b/apps/modules/user.py index ab5728e16..0c07f325f 100644 --- a/apps/modules/user.py +++ b/apps/modules/user.py @@ -59,10 +59,15 @@ def _privacy_settings(self): return { "skills": any( ( + # is public + privacy.skills == PrivacySettings.PrivacyChoices.PUBLIC, + # same user request is own skills is_same_user, + # for hide need to be connected and admin is_connected and is_admin and privacy.skills == PrivacySettings.PrivacyChoices.HIDE, + # user need to be in organization in_organization and privacy.skills == PrivacySettings.PrivacyChoices.ORGANIZATION, ) diff --git a/apps/organizations/serializers.py b/apps/organizations/serializers.py index adcf20e5f..8bf330955 100644 --- a/apps/organizations/serializers.py +++ b/apps/organizations/serializers.py @@ -444,67 +444,7 @@ class Meta: ) -@auto_translated -class ProjectCategoryHierarchySerializer( - OrganizationRelatedSerializer, - serializers.ModelSerializer, -): - children = serializers.SerializerMethodField() - background_image = ImageSerializer(read_only=True) - - is_followed = serializers.SerializerMethodField() - - class Meta: - model = ProjectCategory - read_only_fields = [ - "id", - "slug", - "name", - "background_color", - "foreground_color", - "background_image", - "children", - "is_followed", - ] - fields = read_only_fields - - def get_is_followed(self, category: ProjectCategory) -> dict[str, Any]: - if "request" in self.context: - user = self.context["request"].user - if not user.is_anonymous: - follow = CategoryFollow.objects.filter(follower=user, category=category) - user_follow = follow.first() - if user_follow: - return {"is_followed": True, "follow_id": user_follow.id} - return {"is_followed": False, "follow_id": None} - - def get_children(self, category: ProjectCategory) -> list[dict[str, str | int]]: - context = self.context - mapping = context.get("mapping") - if not mapping: - queryset = ProjectCategory.objects.filter( - organization=category.organization - ) - mapping = {cat.id: cat for cat in queryset} - context["mapping"] = mapping - children_ids = list(category.children.all().values_list("id", flat=True)) - if category.is_root: - children_ids += list( - ProjectCategory.objects.filter( - organization=category.organization, - parent__isnull=True, - is_root=False, - ).values_list("id", flat=True) - ) - children = [mapping.get(child) for child in children_ids if child in mapping] - return ProjectCategoryHierarchySerializer( - children, many=True, context=context - ).data - - # project category - - @auto_translated class ProjectCategorySerializer( StringsImagesSerializer, @@ -654,6 +594,45 @@ class Meta(ProjectCategorySerializer.Meta): ] +@auto_translated +class ProjectCategoryHierarchySerializer(ProjectCategorySerializer): + class Meta(ProjectCategorySerializer.Meta): + read_only_fields = [ + "id", + "slug", + "name", + "background_color", + "foreground_color", + "background_image", + "children", + "is_followed", + ] + fields = read_only_fields + + def get_children(self, category: ProjectCategory) -> list[dict[str, str | int]]: + context = self.context + mapping = context.get("mapping") + if not mapping: + queryset = ProjectCategory.objects.filter( + organization=category.organization + ) + mapping = {cat.id: cat for cat in queryset} + context["mapping"] = mapping + children_ids = list(category.children.all().values_list("id", flat=True)) + if category.is_root: + children_ids += list( + ProjectCategory.objects.filter( + organization=category.organization, + parent__isnull=True, + is_root=False, + ).values_list("id", flat=True) + ) + children = [mapping.get(child) for child in children_ids if child in mapping] + return ProjectCategoryHierarchySerializer( + children, many=True, context=context + ).data + + class CategoryFollowSerializer(serializers.ModelSerializer): category = ProjectCategoryLightSerializer(read_only=True) category_id = serializers.PrimaryKeyRelatedField( From d320690747d869e3386b23f21294904cdc20060d Mon Sep 17 00:00:00 2001 From: rgermain Date: Thu, 10 Sep 2026 15:01:11 +0200 Subject: [PATCH 17/25] fix: skills queryset --- apps/accounts/models.py | 33 +++++++++++++++++++++++ apps/modules/user.py | 57 +-------------------------------------- apps/skills/views.py | 59 ++++++++++++++++++++++++----------------- 3 files changed, 69 insertions(+), 80 deletions(-) diff --git a/apps/accounts/models.py b/apps/accounts/models.py index d8749d02d..1e7c4cde0 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -39,6 +39,7 @@ from apps.newsfeed.models import Event, Instruction, News from apps.organizations.models import Organization from apps.projects.models import AbstractLocation, Project +from apps.skills.models import Skill from services.keycloak.exceptions import RemoteKeycloakAccountNotFound from services.keycloak.interface import KeycloakService from services.keycloak.models import KeycloakAccount @@ -707,6 +708,30 @@ def get_event_related_queryset( **{f"{event_related_name}__in": self.get_event_queryset()} ) + def get_skills_queryset(self) -> QuerySet["Skill"]: + return Skill.objects.filter( + # own user + Q(user__pk=self.pk) + | + # public all user quand see + Q(user__privacy_settings__skills=PrivacySettings.PrivacyChoices.PUBLIC) + # only user in same orga + | Q( + user__privacy_settings__skills=PrivacySettings.PrivacyChoices.ORGANIZATION, + user__groups__organizations__in=self.get_organizations_queryset(), + ) + # only admin/usperadmin + | Q( + user__privacy_settings__skills=PrivacySettings.PrivacyChoices.HIDE, + user__groups__organizations__in=self.get_organizations_queryset(), + user__groups__name="admins", + ) + | Q( + user__privacy_settings__skills=PrivacySettings.PrivacyChoices.HIDE, + user__groups__name="superadmins", + ), + ).distinct() + def can_see_project(self, project: "Project") -> bool: """Whether the user can see the project.""" return self.get_project_queryset().contains(project) @@ -993,6 +1018,11 @@ def get_related_organizations(self) -> list["Organization"]: """Return the organizations related to this model.""" return list(self.get_organizations_queryset()) + def get_skills_queryset(self) -> QuerySet["Skill"]: + return Skill.objects.filter( + user__privacy_setings__skills=PrivacySettings.PrivacyChoices.PUBLIC + ) + class InvitationUser(AnonymousUser): def __init__(self, invitation): @@ -1039,3 +1069,6 @@ def _query_function(self, queryset, *ar, **kw): def get_organizations_queryset(self) -> QuerySet[Organization]: """Return the organizations related to this model.""" return Organization.objects.all() + + def get_skills_queryset(self) -> QuerySet["Skill"]: + return Skill.objects.all() diff --git a/apps/modules/user.py b/apps/modules/user.py index 0c07f325f..cae819f5c 100644 --- a/apps/modules/user.py +++ b/apps/modules/user.py @@ -1,17 +1,13 @@ from functools import cached_property -from django.contrib.auth.models import Group from django.db.models import ( QuerySet, ) from apps.accounts.models import ( - AnonymousUser, PeopleGroup, - PrivacySettings, ProjectUser, ) -from apps.accounts.utils import get_superadmins_group from apps.commons.models import GroupData from apps.files.models import ProjectUserAttachmentFile, ProjectUserAttachmentLink from apps.modules.base import AbstractModules, organization_related, register_module @@ -26,59 +22,8 @@ class UserModules(AbstractModules): instance: ProjectUser - @cached_property - def _privacy_settings(self): - """generate a privacy informations (only for sklls now) to filter queryset""" - - # return privacy filde from user - privacy = self.instance.privacy_settings - - # privacy "ORGANIZATION" - in_organization = self.instance.groups.filter( - organizations__isnull=False, - organizations__in=self.user.get_organizations_queryset(), - ).exists() - - # privacy "hide" - is_connected = self.user.is_authenticated - if isinstance(self.user, AnonymousUser): - is_admin = False - else: - is_admin = self.user.groups.contains(get_superadmins_group()) or ( - Group.objects.filter( - organizations__isnull=False, - organizations__in=self.instance.get_related_organizations(), - name__contains="admins", - users=self.user, - ).exists() - ) - # is same user - is_same_user = self.user.pk == self.instance.pk - - # return boolean for each privacy field - return { - "skills": any( - ( - # is public - privacy.skills == PrivacySettings.PrivacyChoices.PUBLIC, - # same user request is own skills - is_same_user, - # for hide need to be connected and admin - is_connected - and is_admin - and privacy.skills == PrivacySettings.PrivacyChoices.HIDE, - # user need to be in organization - in_organization - and privacy.skills == PrivacySettings.PrivacyChoices.ORGANIZATION, - ) - ) - } - def skills(self) -> QuerySet[Skill]: - qs = self.instance.skills.all() - if self._privacy_settings["skills"]: - return qs - return qs.none() + return self.user.get_skills_queryset().filter(pk__in=self.instance.skills.all()) @organization_related def mentor(self) -> QuerySet[Mentoring]: diff --git a/apps/skills/views.py b/apps/skills/views.py index 5ca2fe9ce..2ffe763d5 100644 --- a/apps/skills/views.py +++ b/apps/skills/views.py @@ -441,9 +441,11 @@ def mentored_skill(self, request, *args, **kwargs): """ Get all skills in current organization that have at least one mentor. """ - skills = Skill.objects.filter( - user__in=self.get_user_queryset(), can_mentor=True - ).distinct() + skills = ( + request.user.get_skills_queryset() + .objects.filter(user__in=self.get_user_queryset(), can_mentor=True) + .distinct() + ) tags = ( Tag.objects.filter(skills__in=skills) .annotate( @@ -485,9 +487,11 @@ def mentoree_skill(self, request, *args, **kwargs): """ Get all skills in current organization that have at least one person who wants to be mentored. """ - skills = Skill.objects.filter( - user__in=self.get_user_queryset(), needs_mentor=True - ).distinct() + skills = ( + request.user.get_skills_queryset() + .filter(user__in=self.get_user_queryset(), needs_mentor=True) + .distinct() + ) tags = ( Tag.objects.filter(skills__in=skills) .annotate( @@ -559,25 +563,29 @@ def mentoree_candidate(self, request, *args, **kwargs): """ Get all users in current organization that have at least one skill that could be mentored by the user. """ - user = get_object_or_404( - self.request.user.get_user_queryset(), id=self.kwargs["user_id"] - ) + user_skills = self.user.modules_by_user( + request.user, self.organization + ).skills() user_mentored_skills = Tag.objects.filter( - skills__user=user, skills__can_mentor=True - ).distinct() - mentorees_skills = Skill.objects.filter( - user__in=self.get_user_queryset(), - needs_mentor=True, - tag__in=user_mentored_skills, + skills__in=user_skills.filter(can_mentor=True) ).distinct() - users = ProjectUser.objects.filter(skills__in=mentorees_skills).annotate( - needs_mentor_on=ArrayAgg( - "skills", - filter=Q( - skills__needs_mentor=True, - skills__tag__in=user_mentored_skills, - ), - distinct=True, + + mentors_skills = request.user.get_skills_queryset().filter( + can_mentor=True, tag__in=user_mentored_skills + ) + users = ( + request.user.get_user_queryset() + .exclude(pk=self.user.pk) + .filter(skills__in=mentors_skills) + .annotate( + needs_mentor_on=ArrayAgg( + "skills", + filter=Q( + skills__needs_mentor=True, + skills__tag__in=user_mentored_skills, + ), + distinct=True, + ) ) ) return self.get_paginated_list(users) @@ -612,16 +620,19 @@ def mentor_candidate(self, request, *args, **kwargs): user_skills = self.user.modules_by_user( request.user, self.organization ).skills() + user_mentoree_skills = Tag.objects.filter( skills__in=user_skills.filter(needs_mentor=True) ).distinct() - mentors_skills = Skill.objects.filter( + mentors_skills = request.user.get_skills_queryset().filter( can_mentor=True, tag__in=user_mentoree_skills ) + users = ( request.user.get_user_queryset() .filter(skills__in=mentors_skills) + .exclude(pk=self.user.pk) .annotate( can_mentor_on=ArrayAgg( "skills", From 56f0936c78c18ea7f44ff363b8deb4301045f2ca Mon Sep 17 00:00:00 2001 From: rgermain Date: Thu, 10 Sep 2026 15:24:34 +0200 Subject: [PATCH 18/25] fix: skills better queryset --- apps/accounts/models.py | 25 ++++++++++++++++--------- 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 ++-- 8 files changed, 30 insertions(+), 23 deletions(-) diff --git a/apps/accounts/models.py b/apps/accounts/models.py index 1e7c4cde0..9cf43f492 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -709,7 +709,17 @@ def get_event_related_queryset( ) def get_skills_queryset(self) -> QuerySet["Skill"]: - return Skill.objects.filter( + if self.is_superuser: + return Skill.objects.all() + + is_org_admin = Group.objects.filter( + organizations__isnull=False, + organizations__in=self.get_organizations_queryset(), + name__contains="admins", + users=self, + ).exists() + + filters = ( # own user Q(user__pk=self.pk) | @@ -720,17 +730,14 @@ def get_skills_queryset(self) -> QuerySet["Skill"]: user__privacy_settings__skills=PrivacySettings.PrivacyChoices.ORGANIZATION, user__groups__organizations__in=self.get_organizations_queryset(), ) - # only admin/usperadmin - | Q( + ) + if is_org_admin: + filters |= Q( user__privacy_settings__skills=PrivacySettings.PrivacyChoices.HIDE, user__groups__organizations__in=self.get_organizations_queryset(), - user__groups__name="admins", ) - | Q( - user__privacy_settings__skills=PrivacySettings.PrivacyChoices.HIDE, - user__groups__name="superadmins", - ), - ).distinct() + + return Skill.objects.filter(filters).distinct() def can_see_project(self, project: "Project") -> bool: """Whether the user can see the project.""" diff --git a/locale/ca/LC_MESSAGES/django.po b/locale/ca/LC_MESSAGES/django.po index 23e91e4c4..ebd9d57fb 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-09 13:51+0200\n" +"POT-Creation-Date: 2026-09-10 15:24+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:166 apps/projects/models.py:170 +#: apps/accounts/models.py:167 apps/projects/models.py:170 msgid "visibility" msgstr "visibilitat" diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index b08c2b4de..42a4128d5 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-09 13:51+0200\n" +"POT-Creation-Date: 2026-09-10 15:24+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:166 apps/projects/models.py:170 +#: apps/accounts/models.py:167 apps/projects/models.py:170 msgid "visibility" msgstr "Sichtbarkeit" diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index 71211020e..8e456b363 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-09 13:51+0200\n" +"POT-Creation-Date: 2026-09-10 15:24+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:166 apps/projects/models.py:170 +#: apps/accounts/models.py:167 apps/projects/models.py:170 msgid "visibility" msgstr "" diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po index 9a34dbbd6..a30ffbb0c 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-09 13:51+0200\n" +"POT-Creation-Date: 2026-09-10 15:24+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:166 apps/projects/models.py:170 +#: apps/accounts/models.py:167 apps/projects/models.py:170 msgid "visibility" msgstr "visibilidad" diff --git a/locale/et/LC_MESSAGES/django.po b/locale/et/LC_MESSAGES/django.po index 7d7807d37..99d0a3863 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-09 13:51+0200\n" +"POT-Creation-Date: 2026-09-10 15:24+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:166 apps/projects/models.py:170 +#: apps/accounts/models.py:167 apps/projects/models.py:170 msgid "visibility" msgstr "nähtavus" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index a0064568f..acd3bb6a3 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-09 13:51+0200\n" +"POT-Creation-Date: 2026-09-10 15:24+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:166 apps/projects/models.py:170 +#: apps/accounts/models.py:167 apps/projects/models.py:170 msgid "visibility" msgstr "visibilité" diff --git a/locale/nl/LC_MESSAGES/django.po b/locale/nl/LC_MESSAGES/django.po index 4fac8cac0..3219ecfad 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-09 13:51+0200\n" +"POT-Creation-Date: 2026-09-10 15:24+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:166 apps/projects/models.py:170 +#: apps/accounts/models.py:167 apps/projects/models.py:170 msgid "visibility" msgstr "zichtbaarheid" From ff56e55fe815839584b2ed54ddb98c2e1602b898 Mon Sep 17 00:00:00 2001 From: rgermain Date: Thu, 10 Sep 2026 15:44:45 +0200 Subject: [PATCH 19/25] fix: skills better queryset --- apps/accounts/models.py | 28 +++++++++++++++++----------- apps/commons/views.py | 8 +------- apps/skills/serializers.py | 1 + 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/apps/accounts/models.py b/apps/accounts/models.py index 9cf43f492..11314a5c3 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -712,13 +712,6 @@ def get_skills_queryset(self) -> QuerySet["Skill"]: if self.is_superuser: return Skill.objects.all() - is_org_admin = Group.objects.filter( - organizations__isnull=False, - organizations__in=self.get_organizations_queryset(), - name__contains="admins", - users=self, - ).exists() - filters = ( # own user Q(user__pk=self.pk) @@ -731,11 +724,24 @@ def get_skills_queryset(self) -> QuerySet["Skill"]: user__groups__organizations__in=self.get_organizations_queryset(), ) ) - if is_org_admin: - filters |= Q( - user__privacy_settings__skills=PrivacySettings.PrivacyChoices.HIDE, - user__groups__organizations__in=self.get_organizations_queryset(), + + org_admin = Group.objects.filter( + Q( + organizations__isnull=False, + organizations__in=self.get_organizations_queryset(), + users=self, + ) + & ( + Q(name__contains=GroupData.Role.ADMINS) + | Q(name__contains=GroupData.Role.FACILITATORS) ) + ) + filters |= Q( + user__privacy_settings__skills=PrivacySettings.PrivacyChoices.HIDE, + user__groups__organizations__in=Organization.objects.filter( + groups__in=org_admin + ), + ) return Skill.objects.filter(filters).distinct() diff --git a/apps/commons/views.py b/apps/commons/views.py index 3b8249926..14a241e2d 100644 --- a/apps/commons/views.py +++ b/apps/commons/views.py @@ -1,7 +1,6 @@ from functools import cached_property from django.db.models import QuerySet -from django.http import Http404 from django.shortcuts import get_object_or_404 from drf_spectacular.utils import OpenApiParameter as _OpenApiParameter from rest_framework import mixins, serializers, viewsets @@ -226,7 +225,6 @@ def initial(self, request, *args, **kwargs): self.user = get_object_or_404( request.user.get_user_queryset().slug_or_id(kwargs["user_id"]), ) - super().initial(request, *args, **kwargs) def get_permissions(self): @@ -242,11 +240,7 @@ def get_serializer_context(self): class NestedOrganizationUserViewMixins( NestedOrganizationViewMixins, NestedUserViewMixins ): - def initial(self, request, *ar, **kw): - super().initial(request, *ar, **kw) - # check if user is in organizations - if not self.user.get_organizations_queryset().contains(self.organization): - raise Http404 + pass class QuerySerializersMixin: diff --git a/apps/skills/serializers.py b/apps/skills/serializers.py index 1a01f61cc..3b251d12c 100644 --- a/apps/skills/serializers.py +++ b/apps/skills/serializers.py @@ -341,6 +341,7 @@ def get_can_mentor_on(self, instance: ProjectUser): if hasattr(instance, "can_mentor_on"): can_mentor_on: list[int] = instance.can_mentor_on skills = Skill.objects.filter(id__in=can_mentor_on) + return SkillLightSerializer(skills, many=True, context=self.context).data return None From f6d12f3baab55a4a956acdc5afb003150991f862 Mon Sep 17 00:00:00 2001 From: rgermain Date: Thu, 10 Sep 2026 16:32:18 +0200 Subject: [PATCH 20/25] fix: notifications queryset --- .../views/test_user_publication_status.py | 9 +++++--- apps/modules/user.py | 13 ++++++++++-- apps/notifications/views.py | 5 +++-- apps/skills/views.py | 21 ++----------------- 4 files changed, 22 insertions(+), 26 deletions(-) diff --git a/apps/accounts/tests/views/test_user_publication_status.py b/apps/accounts/tests/views/test_user_publication_status.py index 6ccc0bc1b..0df69de61 100644 --- a/apps/accounts/tests/views/test_user_publication_status.py +++ b/apps/accounts/tests/views/test_user_publication_status.py @@ -325,6 +325,7 @@ def test_view_users_in_invitations(self, role, expected_users): @parameterized.expand( [ + (TestRoles.DEFAULT, ("public", None, None)), (TestRoles.SUPERADMIN, ("public", "private", "org")), (TestRoles.ORG_ADMIN, ("public", "private", "org")), (TestRoles.ORG_FACILITATOR, ("public", "private", "org")), @@ -350,16 +351,18 @@ def test_view_users_in_notifications(self, role, expected_users): ) self.assertEqual(response.status_code, status.HTTP_200_OK) content = response.json()["results"] - self.assertEqual( { (notification["sender"]["id"], notification["id"]) for notification in content }, { - ((self.users[user_type].id, notifications[user_type].id)) + ( + (self.users[user_type].id, notifications[user_type].id) + if user_type in expected_users + else (None, notifications[user_type].id) + ) for user_type in self.users - if user_type in expected_users }, ) diff --git a/apps/modules/user.py b/apps/modules/user.py index cae819f5c..980d807e1 100644 --- a/apps/modules/user.py +++ b/apps/modules/user.py @@ -10,7 +10,12 @@ ) from apps.commons.models import GroupData from apps.files.models import ProjectUserAttachmentFile, ProjectUserAttachmentLink -from apps.modules.base import AbstractModules, organization_related, register_module +from apps.modules.base import ( + AbstractModules, + ignore_method, + organization_related, + register_module, +) from apps.notifications.models import Notification from apps.organizations.models import CategoryFollow from apps.projects.models import Project @@ -72,8 +77,12 @@ def reviews_projects(self) -> QuerySet[Project]: ) @organization_related + @ignore_method + def all_notifications(self) -> QuerySet[Notification]: + return self.instance.notifications_received.all() + def notifications(self) -> QuerySet[Notification]: - return self.instance.notifications_received.filter(is_viewed=False) + return self.all_notifications().filter(is_viewed=False) @cached_property def _researcher(self) -> Researcher | None: diff --git a/apps/notifications/views.py b/apps/notifications/views.py index 0d363d218..8ea4219c4 100644 --- a/apps/notifications/views.py +++ b/apps/notifications/views.py @@ -48,8 +48,9 @@ class NotificationsViewSet(NestedOrganizationUserViewMixins, ListViewSet): def get_queryset(self): return ( - self.user.modules_by_organization(self.organization) - .notifications() + self.user.modules_by_user(self.request.user, self.organization) + .all_notifications() + .order_by("-created") .select_related("sender", "project", "organization") ) diff --git a/apps/skills/views.py b/apps/skills/views.py index 2ffe763d5..559d7851d 100644 --- a/apps/skills/views.py +++ b/apps/skills/views.py @@ -512,29 +512,12 @@ class UserMentorshipViewset(NestedOrganizationUserViewMixins, PaginatedViewSet): permission_classes = [ReadOnly] def get_user_queryset(self): - request_user = self.request.user user_queryset = self.request.user.get_user_queryset().filter( groups__organizations=self.organization ) - - if request_user.is_authenticated: - if request_user.is_superuser or ( - self.organization.admins.all() | self.organization.facilitators.all() - ).contains(request_user): - return user_queryset - - if user_queryset.contains(request_user): - return user_queryset.filter( - Q( - privacy_settings__skills__in=[ - PrivacySettings.PrivacyChoices.ORGANIZATION, - PrivacySettings.PrivacyChoices.PUBLIC, - ] - ) - ) return user_queryset.filter( - privacy_settings__skills=PrivacySettings.PrivacyChoices.PUBLIC - ) + skills__in=self.request.user.get_skills_queryset() + ).distinct() @extend_schema( parameters=[ From 80fcfcd90151008e2e88d0a3933bc45264da29cd Mon Sep 17 00:00:00 2001 From: rgermain Date: Thu, 10 Sep 2026 16:37:22 +0200 Subject: [PATCH 21/25] fix: skills queryset --- apps/accounts/models.py | 2 +- apps/skills/views.py | 37 +++++++------------------------------ 2 files changed, 8 insertions(+), 31 deletions(-) diff --git a/apps/accounts/models.py b/apps/accounts/models.py index 11314a5c3..bdd101e28 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -1033,7 +1033,7 @@ def get_related_organizations(self) -> list["Organization"]: def get_skills_queryset(self) -> QuerySet["Skill"]: return Skill.objects.filter( - user__privacy_setings__skills=PrivacySettings.PrivacyChoices.PUBLIC + user__privacy_settings__skills=PrivacySettings.PrivacyChoices.PUBLIC ) diff --git a/apps/skills/views.py b/apps/skills/views.py index 559d7851d..9441b86e2 100644 --- a/apps/skills/views.py +++ b/apps/skills/views.py @@ -16,13 +16,14 @@ from rest_framework.request import Request from rest_framework.response import Response -from apps.accounts.models import PrivacySettings, ProjectUser +from apps.accounts.models import ProjectUser from apps.accounts.permissions import HasBasePermission from apps.commons.permissions import IsOwner, ReadOnly, WillBeOwner from apps.commons.utils import map_action_to_permission from apps.commons.views import ( MultipleIDViewsetMixin, NestedOrganizationUserViewMixins, + NestedOrganizationViewMixins, NestedUserViewMixins, PaginatedViewSet, ReadDestroyModelViewSet, @@ -378,41 +379,17 @@ class ReadTagViewSet(viewsets.ReadOnlyModelViewSet): permission_classes = [ReadOnly] -class OrganizationMentorshipViewset(PaginatedViewSet): +class OrganizationMentorshipViewset(NestedOrganizationViewMixins, PaginatedViewSet): serializer_class = TagSerializer permission_classes = [ReadOnly] - def get_organization(self) -> Organization: - organization_code = self.kwargs["organization_code"] - return get_object_or_404(Organization, code=organization_code) - def get_user_queryset(self): - organization = self.get_organization() - request_user = self.request.user - organization_members_id: list[int] = organization.get_all_members().values_list( - "id", flat=True - ) user_queryset = self.request.user.get_user_queryset().filter( - id__in=organization_members_id + groups__organizations=self.organization ) - if request_user.is_authenticated: - if request_user.is_superuser or ( - organization.admins.all() | organization.facilitators.all() - ).contains(request_user): - return user_queryset - if request_user.id in organization_members_id: - return user_queryset.filter( - Q( - privacy_settings__skills__in=[ - PrivacySettings.PrivacyChoices.ORGANIZATION, - PrivacySettings.PrivacyChoices.PUBLIC, - ] - ) - | Q(id=request_user.id) - ) return user_queryset.filter( - privacy_settings__skills=PrivacySettings.PrivacyChoices.PUBLIC - ) + skills__in=self.request.user.get_skills_queryset() + ).distinct() @extend_schema( parameters=[ @@ -443,7 +420,7 @@ def mentored_skill(self, request, *args, **kwargs): """ skills = ( request.user.get_skills_queryset() - .objects.filter(user__in=self.get_user_queryset(), can_mentor=True) + .filter(user__in=self.get_user_queryset(), can_mentor=True) .distinct() ) tags = ( From 6bdc6202025070b01d871e257d104a19d544d999 Mon Sep 17 00:00:00 2001 From: rgermain Date: Thu, 10 Sep 2026 16:43:39 +0200 Subject: [PATCH 22/25] fix: mentors --- apps/skills/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/skills/views.py b/apps/skills/views.py index 9441b86e2..8f988a79a 100644 --- a/apps/skills/views.py +++ b/apps/skills/views.py @@ -531,7 +531,7 @@ def mentoree_candidate(self, request, *args, **kwargs): ).distinct() mentors_skills = request.user.get_skills_queryset().filter( - can_mentor=True, tag__in=user_mentored_skills + needs_mentor=True, tag__in=user_mentored_skills ) users = ( request.user.get_user_queryset() From a176763da76be27ae85096c0b2c34f2dd6ace09b Mon Sep 17 00:00:00 2001 From: rgermain Date: Fri, 11 Sep 2026 10:18:41 +0200 Subject: [PATCH 23/25] fix: tests mentor default --- .../tests/views/test_privacy_settings_fields.py | 2 +- apps/commons/fields.py | 16 +++++++++++----- apps/skills/tests/views/test_user_mentorship.py | 2 -- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/apps/accounts/tests/views/test_privacy_settings_fields.py b/apps/accounts/tests/views/test_privacy_settings_fields.py index 8baee091b..ab4667dfd 100644 --- a/apps/accounts/tests/views/test_privacy_settings_fields.py +++ b/apps/accounts/tests/views/test_privacy_settings_fields.py @@ -86,7 +86,7 @@ def assert_fields_hidden(self, data, skills): (TestRoles.OWNER, PrivacyChoices.HIDE, True), (TestRoles.SUPERADMIN, PrivacyChoices.HIDE, True), (TestRoles.ORG_ADMIN, PrivacyChoices.HIDE, True), - (TestRoles.ORG_FACILITATOR, PrivacyChoices.HIDE, False), + (TestRoles.ORG_FACILITATOR, PrivacyChoices.HIDE, True), (TestRoles.ORG_USER, PrivacyChoices.HIDE, False), (TestRoles.ORG_VIEWER, PrivacyChoices.HIDE, False), ] diff --git a/apps/commons/fields.py b/apps/commons/fields.py index aa102fa49..6421882bb 100644 --- a/apps/commons/fields.py +++ b/apps/commons/fields.py @@ -2,7 +2,7 @@ from contextlib import suppress from django.contrib.auth.models import Group -from django.db.models import QuerySet +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 @@ -13,6 +13,7 @@ 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 @@ -236,10 +237,15 @@ def _check_privacy_settings(self, value): ): return False return Group.objects.filter( - organizations__isnull=False, - organizations__in=instance.get_related_organizations(), - name__contains="admins", - users=request.user, + Q( + organizations__isnull=False, + organizations__in=instance.get_organizations_queryset(), + users=request.user, + ) + & ( + Q(name__contains=GroupData.Role.ADMINS) + | Q(name__contains=GroupData.Role.FACILITATORS) + ) ).exists() return False diff --git a/apps/skills/tests/views/test_user_mentorship.py b/apps/skills/tests/views/test_user_mentorship.py index f83eb0fb4..389411a80 100644 --- a/apps/skills/tests/views/test_user_mentorship.py +++ b/apps/skills/tests/views/test_user_mentorship.py @@ -181,7 +181,6 @@ def setUpTestData(cls): def test_retrieve_mentor_candidates(self, role, mentors): organization = self.organization user = self.get_parameterized_test_user(role, instances=[organization]) - user.groups.add(organization.get_users()) SkillFactory(user=user, tag=self.mentor_skill_1, needs_mentor=True) SkillFactory(user=user, tag=self.mentor_skill_2, needs_mentor=True) @@ -258,7 +257,6 @@ def test_retrieve_mentor_candidates(self, role, mentors): def test_retrieve_mentoree_candidates(self, role, mentorees): organization = self.organization user = self.get_parameterized_test_user(role, instances=[organization]) - user.groups.add(organization.get_users()) SkillFactory(user=user, tag=self.mentoree_skill_1, can_mentor=True) SkillFactory(user=user, tag=self.mentoree_skill_2, can_mentor=True) From 60682c1f3ef02cdf1b65cc9c3ac1c865ac40f537 Mon Sep 17 00:00:00 2001 From: rgermain Date: Fri, 11 Sep 2026 15:26:36 +0200 Subject: [PATCH 24/25] fix: i18n --- locale/ca/LC_MESSAGES/django.po | 8 ++++---- locale/de/LC_MESSAGES/django.po | 8 ++++---- locale/en/LC_MESSAGES/django.po | 8 ++++---- locale/es/LC_MESSAGES/django.po | 8 ++++---- locale/et/LC_MESSAGES/django.po | 8 ++++---- locale/fr/LC_MESSAGES/django.po | 8 ++++---- locale/nl/LC_MESSAGES/django.po | 8 ++++---- 7 files changed, 28 insertions(+), 28 deletions(-) diff --git a/locale/ca/LC_MESSAGES/django.po b/locale/ca/LC_MESSAGES/django.po index ebd9d57fb..b35e99788 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-10 15:24+0200\n" +"POT-Creation-Date: 2026-09-11 15:26+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -176,16 +176,16 @@ msgstr "Estat de publicació desconegut" msgid "Unknown publication status '{publication_status}'" msgstr "Estat de publicació desconegut '{publication_status}'" -#: apps/commons/fields.py:31 apps/skills/serializers.py:112 +#: apps/commons/fields.py:32 apps/skills/serializers.py:112 msgid "This field is required." msgstr "Aquest camp és obligatori." -#: apps/commons/fields.py:32 +#: apps/commons/fields.py:33 #, 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:34 apps/skills/serializers.py:117 +#: apps/commons/fields.py:35 apps/skills/serializers.py:117 #, 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}." diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index 42a4128d5..119257a02 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-10 15:24+0200\n" +"POT-Creation-Date: 2026-09-11 15:26+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -178,16 +178,16 @@ msgstr "Unbekannter Veröffentlichungsstatus" msgid "Unknown publication status '{publication_status}'" msgstr "Unbekannter Veröffentlichungsstatus '{publication_status}'" -#: apps/commons/fields.py:31 apps/skills/serializers.py:112 +#: apps/commons/fields.py:32 apps/skills/serializers.py:112 msgid "This field is required." msgstr "Dieses Feld ist erforderlich." -#: apps/commons/fields.py:32 +#: apps/commons/fields.py:33 #, 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:34 apps/skills/serializers.py:117 +#: apps/commons/fields.py:35 apps/skills/serializers.py:117 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "" diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index 8e456b363..42d0b917e 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-10 15:24+0200\n" +"POT-Creation-Date: 2026-09-11 15:26+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -154,16 +154,16 @@ msgstr "" msgid "Unknown publication status '{publication_status}'" msgstr "" -#: apps/commons/fields.py:31 apps/skills/serializers.py:112 +#: apps/commons/fields.py:32 apps/skills/serializers.py:112 msgid "This field is required." msgstr "" -#: apps/commons/fields.py:32 +#: apps/commons/fields.py:33 #, python-brace-format msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "" -#: apps/commons/fields.py:34 apps/skills/serializers.py:117 +#: apps/commons/fields.py:35 apps/skills/serializers.py:117 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "" diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po index a30ffbb0c..eae485b5d 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-10 15:24+0200\n" +"POT-Creation-Date: 2026-09-11 15:26+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -176,16 +176,16 @@ msgstr "Estado de publicación desconocido" msgid "Unknown publication status '{publication_status}'" msgstr "Estado de publicación desconocido '{publication_status}'" -#: apps/commons/fields.py:31 apps/skills/serializers.py:112 +#: apps/commons/fields.py:32 apps/skills/serializers.py:112 msgid "This field is required." msgstr "Este campo es obligatorio." -#: apps/commons/fields.py:32 +#: apps/commons/fields.py:33 #, 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:34 apps/skills/serializers.py:117 +#: apps/commons/fields.py:35 apps/skills/serializers.py:117 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "" diff --git a/locale/et/LC_MESSAGES/django.po b/locale/et/LC_MESSAGES/django.po index 99d0a3863..561abe9f4 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-10 15:24+0200\n" +"POT-Creation-Date: 2026-09-11 15:26+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -175,16 +175,16 @@ msgstr "Tundmatu avaldamise olek" msgid "Unknown publication status '{publication_status}'" msgstr "Tundmatu avaldamise olek '{publication_status}'" -#: apps/commons/fields.py:31 apps/skills/serializers.py:112 +#: apps/commons/fields.py:32 apps/skills/serializers.py:112 msgid "This field is required." msgstr "See väli on kohustuslik." -#: apps/commons/fields.py:32 +#: apps/commons/fields.py:33 #, python-brace-format msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "Vigane ID \"{user_id}\" - objekti ei eksisteeri." -#: apps/commons/fields.py:34 apps/skills/serializers.py:117 +#: apps/commons/fields.py:35 apps/skills/serializers.py:117 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "Vale tüüp. Oodati stringi väärtust, saadi {data_type}." diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index acd3bb6a3..a0025a266 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-10 15:24+0200\n" +"POT-Creation-Date: 2026-09-11 15:26+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -178,16 +178,16 @@ msgstr "Statut de publication inconnu" msgid "Unknown publication status '{publication_status}'" msgstr "Statut de publication inconnu '{publication_status}'" -#: apps/commons/fields.py:31 apps/skills/serializers.py:112 +#: apps/commons/fields.py:32 apps/skills/serializers.py:112 msgid "This field is required." msgstr "Ce champ est obligatoire." -#: apps/commons/fields.py:32 +#: apps/commons/fields.py:33 #, 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:34 apps/skills/serializers.py:117 +#: apps/commons/fields.py:35 apps/skills/serializers.py:117 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "Type incorrect. Valeur str attendue, {data_type} reçue." diff --git a/locale/nl/LC_MESSAGES/django.po b/locale/nl/LC_MESSAGES/django.po index 3219ecfad..c1d9423c2 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-10 15:24+0200\n" +"POT-Creation-Date: 2026-09-11 15:26+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -178,16 +178,16 @@ msgstr "Onbekende publicatiestatus" msgid "Unknown publication status '{publication_status}'" msgstr "Onbekende publicatiestatus '{publication_status}'" -#: apps/commons/fields.py:31 apps/skills/serializers.py:112 +#: apps/commons/fields.py:32 apps/skills/serializers.py:112 msgid "This field is required." msgstr "Dit veld is verplicht." -#: apps/commons/fields.py:32 +#: apps/commons/fields.py:33 #, python-brace-format msgid "Invalid id \"{user_id}\" - object does not exist." msgstr "Ongeldige id \"{user_id}\" - object bestaat niet." -#: apps/commons/fields.py:34 apps/skills/serializers.py:117 +#: apps/commons/fields.py:35 apps/skills/serializers.py:117 #, python-brace-format msgid "Incorrect type. Expected str value, received {data_type}." msgstr "Onjuist type. Verwachte stringwaarde, ontvangen {data_type}." From 4e6e0214b90632a459cf6552fd5b6c7609272c68 Mon Sep 17 00:00:00 2001 From: rgermain Date: Fri, 11 Sep 2026 16:32:59 +0200 Subject: [PATCH 25/25] test: fix facilitators --- apps/accounts/tests/views/test_privacy_settings_fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/accounts/tests/views/test_privacy_settings_fields.py b/apps/accounts/tests/views/test_privacy_settings_fields.py index ab4667dfd..b2d21506a 100644 --- a/apps/accounts/tests/views/test_privacy_settings_fields.py +++ b/apps/accounts/tests/views/test_privacy_settings_fields.py @@ -138,7 +138,7 @@ def test_view_fields_retrieve_user( (TestRoles.OWNER, PrivacyChoices.HIDE, True), (TestRoles.SUPERADMIN, PrivacyChoices.HIDE, True), (TestRoles.ORG_ADMIN, PrivacyChoices.HIDE, True), - (TestRoles.ORG_FACILITATOR, PrivacyChoices.HIDE, False), + (TestRoles.ORG_FACILITATOR, PrivacyChoices.HIDE, True), (TestRoles.ORG_USER, PrivacyChoices.HIDE, False), ] )