From 019f2373a3e7393570519d17ff35c331ca7e8286 Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 24 Jun 2026 12:05:28 +0200 Subject: [PATCH 01/13] refactor: optimize file url --- apps/files/models.py | 24 ++++++++++++++++++++++++ apps/files/serializers.py | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/apps/files/models.py b/apps/files/models.py index 6e9d1729..16d10335 100644 --- a/apps/files/models.py +++ b/apps/files/models.py @@ -6,6 +6,7 @@ from azure.core.exceptions import ResourceNotFoundError from django.apps import apps from django.conf import settings +from django.core.cache import cache from django.core.files.uploadedfile import SimpleUploadedFile from django.db import models, transaction from django.db.models import ForeignObjectRel, Model, Q, QuerySet @@ -253,6 +254,25 @@ class Meta: ordering = ("-created_at",) abstract = True + @property + def url_key(self) -> str: + return f"image::url::{self.pk}" + + @property + def url(self) -> str: + """create cache for url file""" + value = cache.get(self.url_key) + if value: + return value + + try: + value = self.file.url + except AttributeError: + return "" + + cache.set(self.url_key, value) + return value + def duplicate(self, upload_to: str = "", **fields) -> None | Self: with suppress(ResourceNotFoundError): file_path = self.file.name.split("/") @@ -271,6 +291,10 @@ def duplicate(self, upload_to: str = "", **fields) -> None | Self: return super().duplicate(_upload_to=_upload_to, file=new_file, **fields) return None + def save(self, *ar, **kw): + cache.delete(self.url_key) + return super().save(*ar, **kw) + class Image(BaseImage, HasOwner, ProjectRelated, OrganizationRelated): name = models.CharField(max_length=255) diff --git a/apps/files/serializers.py b/apps/files/serializers.py index a7076d4c..e0c1fd75 100644 --- a/apps/files/serializers.py +++ b/apps/files/serializers.py @@ -415,7 +415,7 @@ def validate_file(self, file): def get_url(self, image: Image) -> str | None: try: - url = image.file.url + url = image.url except AttributeError: return None request = self.context.get("request") From 7691e43cedc28a0ec3bf7ea734377d660ee401b5 Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 24 Jun 2026 12:09:20 +0200 Subject: [PATCH 02/13] refactor: optimize permissions --- apps/organizations/permissions.py | 8 +++++ apps/projects/permissions.py | 57 +++++++++++++++++++------------ 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/apps/organizations/permissions.py b/apps/organizations/permissions.py index dd762889..68ecd4ed 100644 --- a/apps/organizations/permissions.py +++ b/apps/organizations/permissions.py @@ -1,3 +1,5 @@ +from functools import cache + from django.db.models import Model from rest_framework import permissions from rest_framework.generics import get_object_or_404 @@ -18,6 +20,12 @@ class OrganizationRelatedPermission(IgnoreCall): + def __init__(self, *ar, **kw): + super().__init__(*ar, **kw) + + # add locale cache + self.get_related_organizations = cache(self.get_related_organizations) + def get_related_organizations( self, request: Request, view: GenericViewSet, obj: Model = None ) -> list[Organization]: diff --git a/apps/projects/permissions.py b/apps/projects/permissions.py index e002e91c..4b03b51e 100644 --- a/apps/projects/permissions.py +++ b/apps/projects/permissions.py @@ -1,4 +1,7 @@ +from collections.abc import Generator from contextlib import suppress +from functools import cache +from typing import Any from django.db.models import Model from rest_framework import permissions @@ -16,6 +19,12 @@ class ProjectRelatedPermission(IgnoreCall): + def __init__(self, *ar, **kw): + super().__init__(*ar, **kw) + + # add locale cache + self.get_related_project = cache(self.get_related_project) + def get_related_project( self, request: Request, view: GenericViewSet, obj: Model | None = None ) -> Project | None: @@ -55,48 +64,52 @@ def HasProjectPermission( # noqa: N802 codename: str, app: str = "projects" ) -> permissions.BasePermission: class _HasProjectPermission(permissions.BasePermission, ProjectRelatedPermission): - def has_permission(self, request: Request, view: GenericViewSet) -> bool: - if request.user.is_authenticated: - project = self.get_related_project(request, view) - if project and app: - return request.user.has_perm(f"{app}.{codename}", project) - if project: - return request.user.has_perm(codename, project) - return False + def __init__(self, *ar, **kw): + super().__init__(*ar, **kw) - def has_object_permission( - self, request: Request, view: GenericViewSet, obj: Model + self.has_permission = cache(self.has_permission) + + def has_permission( + self, request: Request, view: GenericViewSet, project: Model = None ) -> bool: if request.user.is_authenticated: - project = self.get_related_project(request, view, obj) # If get_related_project returns None with a non-null obj, it might be # because the object is not yet linked to a project. In that case, it is # relevant to retry the permission check with the project_id in the URL. if not project: project = self.get_related_project(request, view) if project and app: - request.user.has_perm(f"{app}.{codename}", project) + return request.user.has_perm(f"{app}.{codename}", project) if project: return request.user.has_perm(codename, project) return False + def has_object_permission( + self, request: Request, view: GenericViewSet, obj: Model + ) -> bool: + return self.has_permission(request, view, obj) + return _HasProjectPermission class ProjectIsNotLocked(permissions.BasePermission, ProjectRelatedPermission): + def __init__(self, *ar, **kw): + super().__init__(*ar, **kw) + + self.cache_iter_perms = cache(self.user_can_modify_locked_project) + + def iter_perms( + self, project: Project, user: ProjectUser + ) -> Generator[tuple[bool], Any, Any]: + yield user.has_perm("projects.change_locked_project"), + yield user.has_perm("projects.change_locked_project", project), + for o in project.get_related_organizations(): + yield user.has_perm("organizations.change_locked_project", o) + def user_can_modify_locked_project( self, project: Project, user: ProjectUser ) -> bool: - return any( - [ - user.has_perm("projects.change_locked_project"), - user.has_perm("projects.change_locked_project", project), - *[ - user.has_perm("organizations.change_locked_project", o) - for o in project.get_related_organizations() - ], - ] - ) + return any(self.iter_perms(project, user)) def has_permission(self, request: Request, view: GenericViewSet) -> bool: return self.has_object_permission(request, view, None) From d3a7d60a86a6baa227426686cd051ecda66181d9 Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 24 Jun 2026 12:09:55 +0200 Subject: [PATCH 03/13] refactor: optimize projects view/serializer --- apps/organizations/serializers.py | 18 +++--------------- apps/projects/serializers.py | 4 ++-- apps/projects/views.py | 2 +- 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/apps/organizations/serializers.py b/apps/organizations/serializers.py index 451c8ba6..b8368b99 100644 --- a/apps/organizations/serializers.py +++ b/apps/organizations/serializers.py @@ -367,7 +367,7 @@ def get_attachment_files_count(self, organization: Organization) -> int: def create(self, validated_data): team = validated_data.pop("team", {}) - organization = super(OrganizationSerializer, self).create(validated_data) + organization = super().create(validated_data) OrganizationAddTeamMembersSerializer().create( {"organization": organization, **team} ) @@ -375,16 +375,13 @@ def create(self, validated_data): def update(self, instance, validated_data): validated_data.pop("team", {}) - return super(OrganizationSerializer, self).update(instance, validated_data) + return super().update(instance, validated_data) @auto_translated class OrganizationLightSerializer( - OrganizationRelatedSerializer, - serializers.ModelSerializer, + OrganizationSerializer, ): - logo_image = ImageSerializer(read_only=True) - class Meta: model = Organization fields = [ @@ -399,15 +396,6 @@ class Meta: "is_logo_visible_on_parent_dashboard", ] - def get_related_organizations(self) -> Organization: - # We're not supposed to be here since only super admin can create - # organization and this function should not be called in this case. - # see the view and permissions associated with this serializer. - # - # We return a dummy value so that the permission process can continue - # and return a `False`. - return [SimpleNamespace(code=uuid.uuid4(), pk=uuid.uuid4())] - def create(self, validated_data): """Create the instance's permissions and default groups.""" organization = super().create(validated_data) diff --git a/apps/projects/serializers.py b/apps/projects/serializers.py index e9177de5..09fbf4f3 100644 --- a/apps/projects/serializers.py +++ b/apps/projects/serializers.py @@ -34,7 +34,7 @@ from apps.notifications.tasks import notify_new_project, notify_project_changes from apps.organizations.models import Organization, ProjectCategory, Template from apps.organizations.serializers import ( - OrganizationSerializer, + OrganizationLightSerializer, ProjectCategoryLightSerializer, ProjectTemplateSerializer, ) @@ -199,7 +199,7 @@ class ProjectSerializer( # read_only header_image = ImageSerializer(read_only=True) categories = ProjectCategoryLightSerializer(many=True, read_only=True) - organizations = OrganizationSerializer(many=True, read_only=True) + organizations = OrganizationLightSerializer(many=True, read_only=True) template = ProjectTemplateSerializer(read_only=True) views = serializers.SerializerMethodField() diff --git a/apps/projects/views.py b/apps/projects/views.py index c0d273f4..997b3f81 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -117,7 +117,7 @@ def get_queryset(self) -> QuerySet: .prefetch_related( "categories", "tags", - "organizations", + "organizations__logo_image", ) ) From 92f746018ed328f75b922f3f2b43de6ada3b0c5e Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 24 Jun 2026 12:57:27 +0200 Subject: [PATCH 04/13] refactor: optimize images --- apps/files/models.py | 52 ++++++++++++++++++++++++++++-------- apps/files/serializers.py | 55 ++++----------------------------------- 2 files changed, 46 insertions(+), 61 deletions(-) diff --git a/apps/files/models.py b/apps/files/models.py index 16d10335..6c92dc69 100644 --- a/apps/files/models.py +++ b/apps/files/models.py @@ -9,7 +9,7 @@ from django.core.cache import cache from django.core.files.uploadedfile import SimpleUploadedFile from django.db import models, transaction -from django.db.models import ForeignObjectRel, Model, Q, QuerySet +from django.db.models import ForeignObjectRel, ImageField, Model, Q, QuerySet from django.utils import timezone from simple_history.models import HistoricalRecords from stdimage import StdImageField @@ -254,25 +254,55 @@ class Meta: ordering = ("-created_at",) abstract = True - @property - def url_key(self) -> str: - return f"image::url::{self.pk}" - - @property - def url(self) -> str: + @staticmethod + def get_url(cache_key: str, field: ImageField) -> str: """create cache for url file""" - value = cache.get(self.url_key) + value = cache.get(cache_key) if value: return value try: - value = self.file.url + value = field.url except AttributeError: return "" - cache.set(self.url_key, value) + cache.set(cache_key, value) return value + @property + def __url_key(self) -> str: + return f"image::url::{self.pk}" + + @property + def __url_variations_key(self) -> dict[str, str]: + field = self.file.field + + obj = {} + for name in field.variations.keys(): + obj[name] = f"image::url::{name}::{self.pk}" + return obj + + @property + def url(self) -> str: + """create cache for url file""" + return self.get_url(self.__url_key, self.file) + + @property + def variations(self) -> dict[str, str]: + varias = {"original": self.url} + + for name, key in self.__url_variations_key.items(): + field = getattr(self.file, name, None) + + url = "" + if field: + url = self.get_url(key, field) + varias[name] = url + return varias + + def clear_cache_urls(self): + cache.delete_many(self.__url_key, *list(self.__url_variations_key.values())) + def duplicate(self, upload_to: str = "", **fields) -> None | Self: with suppress(ResourceNotFoundError): file_path = self.file.name.split("/") @@ -292,7 +322,7 @@ def duplicate(self, upload_to: str = "", **fields) -> None | Self: return None def save(self, *ar, **kw): - cache.delete(self.url_key) + self.clear_cache_urls() return super().save(*ar, **kw) diff --git a/apps/files/serializers.py b/apps/files/serializers.py index e0c1fd75..e6bf6868 100644 --- a/apps/files/serializers.py +++ b/apps/files/serializers.py @@ -10,6 +10,7 @@ from django.http import QueryDict from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator +from services.translator.serializers import auto_translated from apps.commons.serializers import ( OrganizationRelatedSerializer, @@ -18,7 +19,6 @@ ) from apps.organizations.models import Organization from apps.projects.models import Project -from services.translator.serializers import auto_translated from .exceptions import ( ChangeFileProjectError, @@ -40,54 +40,6 @@ ) -# From https://github.com/glemmaPaul/django-stdimage-serializer (however the repo is not maintained anymore) -class StdImageField(serializers.ImageField): - """ - Get all the variations of the StdImageField - """ - - def to_native(self, obj): - return self.get_variations_urls(obj) - - def to_representation(self, obj): - return self.get_variations_urls(obj) - - def get_variations_urls(self, obj): - """ - Get all the logo urls. - """ - - # Initiate return object - return_object = {} - - # Get the field of the object - field = obj.field - - # A lot of ifs going araound, first check if it has the field variations - if hasattr(field, "variations"): - # Get the variations - variations = field.variations - # Go through the variations dict - for key in variations.keys(): - # Just to be sure if the stdimage object has it stored in the obj - if hasattr(obj, key): - # get the by stdimage properties - field_obj = getattr(obj, key, None) - if field_obj and hasattr(field_obj, "url"): - # store it, with the name of the variation type into our return object - return_object[key] = super( - StdImageField, self - ).to_representation(field_obj) - - # Also include the original (if possible) - if hasattr(obj, "url"): - return_object["original"] = super(StdImageField, self).to_representation( - obj - ) - - return return_object - - class AbstractAttachmentLink(metaclass=serializers.SerializerMetaclass): preview_image_url = serializers.URLField(max_length=2048, read_only=True) site_name = serializers.CharField(max_length=255, read_only=True) @@ -386,8 +338,8 @@ def get_related_project(self) -> Optional["Project"]: class ImageSerializer(serializers.ModelSerializer): url = serializers.SerializerMethodField() + variations = serializers.SerializerMethodField() file = serializers.ImageField(write_only=True) - variations = StdImageField(source="file", read_only=True) class Meta: model = Image @@ -407,6 +359,9 @@ class Meta: "variations", ] + def get_variations(self, file): + return file.variations + def validate_file(self, file): limit = settings.MAX_FILE_SIZE * 1024 * 1024 if file.size > limit: From 62017e6dd1f6a340e7ada22d921a0479a3426e85 Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 24 Jun 2026 13:16:46 +0200 Subject: [PATCH 05/13] linter --- apps/files/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/files/serializers.py b/apps/files/serializers.py index e6bf6868..d6c10786 100644 --- a/apps/files/serializers.py +++ b/apps/files/serializers.py @@ -10,7 +10,6 @@ from django.http import QueryDict from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator -from services.translator.serializers import auto_translated from apps.commons.serializers import ( OrganizationRelatedSerializer, @@ -19,6 +18,7 @@ ) from apps.organizations.models import Organization from apps.projects.models import Project +from services.translator.serializers import auto_translated from .exceptions import ( ChangeFileProjectError, From 862c866c58a5a26258548e07262926a1258c91d1 Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 24 Jun 2026 15:16:59 +0200 Subject: [PATCH 06/13] fix: deletes keys --- apps/files/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/files/models.py b/apps/files/models.py index 6c92dc69..d660df57 100644 --- a/apps/files/models.py +++ b/apps/files/models.py @@ -301,7 +301,7 @@ def variations(self) -> dict[str, str]: return varias def clear_cache_urls(self): - cache.delete_many(self.__url_key, *list(self.__url_variations_key.values())) + cache.delete_many([self.__url_key, *list(self.__url_variations_key.values())]) def duplicate(self, upload_to: str = "", **fields) -> None | Self: with suppress(ResourceNotFoundError): From 2b7cf6d10064c741355bf31267ba0458c4316ec4 Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 24 Jun 2026 15:43:38 +0200 Subject: [PATCH 07/13] optimize: modules members/groups --- apps/modules/group.py | 34 ++++++++++----------- apps/modules/project.py | 67 +++++++++++++++++------------------------ 2 files changed, 44 insertions(+), 57 deletions(-) diff --git a/apps/modules/group.py b/apps/modules/group.py index 6604fe43..119d21f4 100644 --- a/apps/modules/group.py +++ b/apps/modules/group.py @@ -15,33 +15,33 @@ class PeopleGroupModules(AbstractModules): instance: PeopleGroup def members(self) -> QuerySet[ProjectUser]: + def queryset_users(role: GroupData.Role, priority_role_order: int): + group_data = GroupData.objects.filter( + role=role, group__people_groups=self.instance + ) + + return ProjectUser.objects.filter(groups__data__in=group_data).annotate( + role=Value(role), priority_role_order=Value(priority_role_order) + ) + skills_prefetch = Prefetch( "skills", queryset=Skill.objects.select_related("tag") ) - leaders = self.instance.leaders.all() - managers = self.instance.managers.all() - members = self.instance.members.all() + # get all members and annote rolepeople_groups + leaders = queryset_users(GroupData.Role.LEADERS, 1) + managers = queryset_users(GroupData.Role.MANAGERS, 2) + members = queryset_users(GroupData.Role.MEMBERS, 3) + # union all and filter by request.user all_members = leaders | managers | members + return ( all_members.distinct() .filter(pk__in=self.user.get_user_queryset()) - .annotate( - role=Case( - When(pk__in=leaders, then=Value(GroupData.Role.LEADERS)), - When(pk__in=managers, then=Value(GroupData.Role.MANAGERS)), - When(pk__in=members, then=Value(GroupData.Role.MEMBERS)), - ), - # add sort order priority (first leader, manager and members) - priority_role_order=Case( - When(pk__in=leaders, then=1), - When(pk__in=managers, then=2), - When(pk__in=members, then=3), - ), - ) + .prefetch_related(skills_prefetch) .order_by("priority_role_order") - .prefetch_related(skills_prefetch, "groups") + .distinct() ) def featured_projects(self) -> QuerySet[Project]: diff --git a/apps/modules/project.py b/apps/modules/project.py index 9c7e8aa5..58b368ba 100644 --- a/apps/modules/project.py +++ b/apps/modules/project.py @@ -1,4 +1,4 @@ -from django.db.models import Case, QuerySet, Value, When +from django.db.models import QuerySet, Value from apps.accounts.models import PeopleGroup, ProjectUser from apps.announcements.models import Announcement @@ -9,6 +9,7 @@ from apps.projects.models import ( BlogEntry, Goal, + LinkedProject, Location, Project, ProjectMessage, @@ -20,62 +21,48 @@ class ProjectModules(AbstractModules): instance: Project def members(self) -> QuerySet[ProjectUser]: + + def queryset_users(role: GroupData.Role, priority_role_order: int): + group_data = GroupData.objects.filter( + role=role, group__projects=self.instance + ) + return ProjectUser.objects.filter(groups__data__in=group_data).annotate( + role=Value(role), priority_role_order=Value(priority_role_order) + ) + # get all members and annote role - owners = self.instance.owners.all() - members = self.instance.members.all() - reviewers = self.instance.reviewers.all() + owners = queryset_users(GroupData.Role.OWNERS, 1) + members = queryset_users(GroupData.Role.MEMBERS, 2) + reviewers = queryset_users(GroupData.Role.REVIEWERS, 3) # union all and filter by request.user all_members = owners | members | reviewers return ( all_members.distinct() .filter(pk__in=self.user.get_user_queryset()) - .annotate( - role=Case( - When(pk__in=owners, then=Value(GroupData.Role.OWNERS)), - When(pk__in=members, then=Value(GroupData.Role.MEMBERS)), - When(pk__in=reviewers, then=Value(GroupData.Role.REVIEWERS)), - ), - # add sort order priority (first leader, manager and members) - priority_role_order=Case( - When(pk__in=owners, then=1), - When(pk__in=members, then=2), - When(pk__in=reviewers, then=3), - ), - ) .order_by("priority_role_order") .distinct() ) def groups(self) -> QuerySet[PeopleGroup]: + def queryset_groups(role: GroupData.Role, priority_role_order: int): + group_data = GroupData.objects.filter( + role=role, group__projects=self.instance + ) + return PeopleGroup.objects.filter(groups__data__in=group_data).annotate( + role=Value(role), priority_role_order=Value(priority_role_order) + ) + # get all members and annote role - owner_groups = self.instance.owner_groups.all() - member_groups = self.instance.member_groups.all() - reviewer_groups = self.instance.reviewer_groups.all() + owner_groups = queryset_groups(GroupData.Role.OWNER_GROUPS, 1) + member_groups = queryset_groups(GroupData.Role.MEMBER_GROUPS, 2) + reviewer_groups = queryset_groups(GroupData.Role.REVIEWER_GROUPS, 3) # union all and filter by request.user - all_members = owner_groups | member_groups | reviewer_groups + all_groups = owner_groups | member_groups | reviewer_groups return ( - all_members.distinct() + all_groups.distinct() .filter(pk__in=self.user.get_people_group_queryset()) - .annotate( - role=Case( - When(pk__in=owner_groups, then=Value(GroupData.Role.OWNER_GROUPS)), - When( - pk__in=member_groups, then=Value(GroupData.Role.MEMBER_GROUPS) - ), - When( - pk__in=reviewer_groups, - then=Value(GroupData.Role.REVIEWER_GROUPS), - ), - ), - # add sort order priority (first leader, manager and members) - priority_role_order=Case( - When(pk__in=owner_groups, then=1), - When(pk__in=member_groups, then=2), - When(pk__in=reviewer_groups, then=3), - ), - ) .order_by("priority_role_order") .distinct() ) From 2d2ecd274901f0fc60b0fcbdd6da3770c9e7ba7a Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 24 Jun 2026 15:47:48 +0200 Subject: [PATCH 08/13] refactor: modules by user --- apps/accounts/views.py | 28 +++++++--------------------- apps/files/views.py | 4 +--- services/crisalid/views.py | 6 +++--- 3 files changed, 11 insertions(+), 27 deletions(-) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 9e94705a..1b6f6fed 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -704,9 +704,7 @@ def remove_member(self, request, *args, **kwargs): def member(self, request, *args, **kwargs): group = self.get_object() - modules_manager = group.get_related_module() - modules = modules_manager(group, request.user) - queryset = modules.members() + queryset = group.modules_by_user(request.user).members() page = self.paginate_queryset(queryset) if page is not None: @@ -795,9 +793,7 @@ def remove_featured_project(self, request, *args, **kwargs): ) def project(self, request, *args, **kwargs): group = self.get_object() - modules_manager = group.get_related_module() - modules = modules_manager(group, request.user) - queryset = modules.featured_projects() + queryset = group.modules_by_user(request.user).featured_projects() page = self.paginate_queryset(queryset) project_serializer = ProjectLightSerializer( @@ -842,9 +838,7 @@ def hierarchy(self, request, *args, **kwargs): ) def subgroups(self, request, *args, **kwargs): group = self.get_object() - modules_manager = group.get_related_module() - modules = modules_manager(group, request.user) - queryset = modules.subgroups() + queryset = group.modules_by_user(request.user).subgroups() queryset_page = self.paginate_queryset(queryset) data = PeopleGroupLightSerializer( @@ -861,9 +855,7 @@ def subgroups(self, request, *args, **kwargs): ) def similars(self, request, *args, **kwargs): group = self.get_object() - modules_manager = group.get_related_module() - modules = modules_manager(group, request.user) - queryset = modules.similars() + queryset = group.modules_by_user(request.user).similars() queryset_page = self.paginate_queryset(queryset) data = PeopleGroupLightSerializer( @@ -880,9 +872,7 @@ def similars(self, request, *args, **kwargs): ) def locations(self, request, *args, **kwargs): group = self.get_object() - modules_manager = group.get_related_module() - modules = modules_manager(group, request.user) - queryset = modules.locations() + queryset = group.modules_by_user(request.user).locations() return Response( LocationSerializer(queryset, many=True, context={"request": request}).data, @@ -898,9 +888,7 @@ def locations(self, request, *args, **kwargs): ) def news(self, request, *args, **kwargs): group = self.get_object() - modules_manager = group.get_related_module() - modules = modules_manager(group, request.user) - queryset = modules.news() + queryset = group.modules_by_user(request.user).news() # use NewsViewSet to filter/order events queryset = NewsViewSet(request=self.request).filter_queryset(queryset) @@ -920,9 +908,7 @@ def news(self, request, *args, **kwargs): ) def event(self, request, *args, **kwargs): group = self.get_object() - modules_manager = group.get_related_module() - modules = modules_manager(group, request.user) - queryset = modules.event() + queryset = group.modules_by_user(request.user).event() # use EventViewSet to filter/order events queryset = EventViewSet(request=self.request).filter_queryset(queryset) diff --git a/apps/files/views.py b/apps/files/views.py index 80c04de1..5e35f0bb 100644 --- a/apps/files/views.py +++ b/apps/files/views.py @@ -290,9 +290,7 @@ def get_permissions(self): return super().get_permissions() def get_queryset(self): - modules_manager = self.people_group.get_related_module() - modules = modules_manager(self.people_group, self.request.user) - return modules.gallery() + return self.people_group.modules_by_user(self.request.user).gallery() def update(self, request, *ar, **kw): request.data["people_group"] = self.people_group.id diff --git a/services/crisalid/views.py b/services/crisalid/views.py index f4dcb139..3138bf5e 100644 --- a/services/crisalid/views.py +++ b/services/crisalid/views.py @@ -211,9 +211,9 @@ class AbstractGroupDocumentViewSet( NestedPeopleGroupViewMixins, AbstractDocumentViewSet ): def get_queryset(self): - modules_manager = self.people_group.get_related_module() - modules = modules_manager(self.people_group, self.request.user) - return getattr(modules, self.document_name)() + return getattr( + self.people_group.modules_by_user(self.request.user), self.document_name + )() class AbstractResearcherDocumentViewSet( From dcd58228b8ba6c1c2fe4f0f6e5060aa9300eefb9 Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 24 Jun 2026 16:05:50 +0200 Subject: [PATCH 09/13] fix: role members/groups --- apps/modules/group.py | 29 +++++++++++++------ apps/modules/project.py | 63 +++++++++++++++++++++++++++++------------ 2 files changed, 66 insertions(+), 26 deletions(-) diff --git a/apps/modules/group.py b/apps/modules/group.py index 119d21f4..b279b8d3 100644 --- a/apps/modules/group.py +++ b/apps/modules/group.py @@ -15,23 +15,21 @@ class PeopleGroupModules(AbstractModules): instance: PeopleGroup def members(self) -> QuerySet[ProjectUser]: - def queryset_users(role: GroupData.Role, priority_role_order: int): + def queryset_users(role: GroupData.Role): group_data = GroupData.objects.filter( role=role, group__people_groups=self.instance ) - return ProjectUser.objects.filter(groups__data__in=group_data).annotate( - role=Value(role), priority_role_order=Value(priority_role_order) - ) + return ProjectUser.objects.filter(groups__data__in=group_data) skills_prefetch = Prefetch( "skills", queryset=Skill.objects.select_related("tag") ) # get all members and annote rolepeople_groups - leaders = queryset_users(GroupData.Role.LEADERS, 1) - managers = queryset_users(GroupData.Role.MANAGERS, 2) - members = queryset_users(GroupData.Role.MEMBERS, 3) + leaders = queryset_users(GroupData.Role.LEADERS) + managers = queryset_users(GroupData.Role.MANAGERS) + members = queryset_users(GroupData.Role.MEMBERS) # union all and filter by request.user all_members = leaders | managers | members @@ -40,8 +38,23 @@ def queryset_users(role: GroupData.Role, priority_role_order: int): all_members.distinct() .filter(pk__in=self.user.get_user_queryset()) .prefetch_related(skills_prefetch) + .annotate( + role=Case( + When(pk__in=leaders, then=Value(GroupData.Role.LEADERS)), + When(pk__in=managers, then=Value(GroupData.Role.MANAGERS)), + When( + pk__in=members, + then=Value(GroupData.Role.MEMBERS), + ), + ), + # add sort order priority (first leader, manager and members) + priority_role_order=Case( + When(pk__in=leaders, then=1), + When(pk__in=managers, then=2), + When(pk__in=members, then=3), + ), + ) .order_by("priority_role_order") - .distinct() ) def featured_projects(self) -> QuerySet[Project]: diff --git a/apps/modules/project.py b/apps/modules/project.py index 58b368ba..a4a283e4 100644 --- a/apps/modules/project.py +++ b/apps/modules/project.py @@ -1,4 +1,4 @@ -from django.db.models import QuerySet, Value +from django.db.models import Case, QuerySet, Value, When from apps.accounts.models import PeopleGroup, ProjectUser from apps.announcements.models import Announcement @@ -9,7 +9,6 @@ from apps.projects.models import ( BlogEntry, Goal, - LinkedProject, Location, Project, ProjectMessage, @@ -22,49 +21,77 @@ class ProjectModules(AbstractModules): def members(self) -> QuerySet[ProjectUser]: - def queryset_users(role: GroupData.Role, priority_role_order: int): + def queryset_users(role: GroupData.Role): group_data = GroupData.objects.filter( role=role, group__projects=self.instance ) - return ProjectUser.objects.filter(groups__data__in=group_data).annotate( - role=Value(role), priority_role_order=Value(priority_role_order) - ) + return ProjectUser.objects.filter(groups__data__in=group_data) # get all members and annote role - owners = queryset_users(GroupData.Role.OWNERS, 1) - members = queryset_users(GroupData.Role.MEMBERS, 2) - reviewers = queryset_users(GroupData.Role.REVIEWERS, 3) + owners = queryset_users(GroupData.Role.OWNERS) + members = queryset_users(GroupData.Role.MEMBERS) + reviewers = queryset_users(GroupData.Role.REVIEWERS) # union all and filter by request.user all_members = owners | members | reviewers return ( all_members.distinct() .filter(pk__in=self.user.get_user_queryset()) + .annotate( + role=Case( + When(pk__in=owners, then=Value(GroupData.Role.OWNERS)), + When(pk__in=members, then=Value(GroupData.Role.MEMBERS)), + When( + pk__in=reviewers, + then=Value(GroupData.Role.REVIEWERS), + ), + ), + # add sort order priority (first leader, manager and members) + priority_role_order=Case( + When(pk__in=owners, then=1), + When(pk__in=members, then=2), + When(pk__in=reviewers, then=3), + ), + ) .order_by("priority_role_order") - .distinct() ) def groups(self) -> QuerySet[PeopleGroup]: - def queryset_groups(role: GroupData.Role, priority_role_order: int): + def queryset_groups(role: GroupData.Role): group_data = GroupData.objects.filter( role=role, group__projects=self.instance ) - return PeopleGroup.objects.filter(groups__data__in=group_data).annotate( - role=Value(role), priority_role_order=Value(priority_role_order) - ) + return PeopleGroup.objects.filter(groups__data__in=group_data) # get all members and annote role - owner_groups = queryset_groups(GroupData.Role.OWNER_GROUPS, 1) - member_groups = queryset_groups(GroupData.Role.MEMBER_GROUPS, 2) - reviewer_groups = queryset_groups(GroupData.Role.REVIEWER_GROUPS, 3) + owner_groups = queryset_groups(GroupData.Role.OWNER_GROUPS) + member_groups = queryset_groups(GroupData.Role.MEMBER_GROUPS) + reviewer_groups = queryset_groups(GroupData.Role.REVIEWER_GROUPS) # union all and filter by request.user all_groups = owner_groups | member_groups | reviewer_groups return ( all_groups.distinct() .filter(pk__in=self.user.get_people_group_queryset()) + .annotate( + role=Case( + When(pk__in=owner_groups, then=Value(GroupData.Role.OWNER_GROUPS)), + When( + pk__in=member_groups, then=Value(GroupData.Role.MEMBER_GROUPS) + ), + When( + pk__in=reviewer_groups, + then=Value(GroupData.Role.REVIEWER_GROUPS), + ), + ), + # add sort order priority (first leader, manager and members) + priority_role_order=Case( + When(pk__in=owner_groups, then=1), + When(pk__in=member_groups, then=2), + When(pk__in=reviewer_groups, then=3), + ), + ) .order_by("priority_role_order") - .distinct() ) def linked_projects(self) -> QuerySet[Project]: From 072e105216fbb3c65f087fcfe5a76f355ca35fba Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 24 Jun 2026 16:11:43 +0200 Subject: [PATCH 10/13] linter --- apps/modules/group.py | 7 ++----- apps/modules/project.py | 7 +++---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/apps/modules/group.py b/apps/modules/group.py index b279b8d3..6f623fce 100644 --- a/apps/modules/group.py +++ b/apps/modules/group.py @@ -37,15 +37,11 @@ def queryset_users(role: GroupData.Role): return ( all_members.distinct() .filter(pk__in=self.user.get_user_queryset()) - .prefetch_related(skills_prefetch) .annotate( role=Case( When(pk__in=leaders, then=Value(GroupData.Role.LEADERS)), When(pk__in=managers, then=Value(GroupData.Role.MANAGERS)), - When( - pk__in=members, - then=Value(GroupData.Role.MEMBERS), - ), + When(pk__in=members, then=Value(GroupData.Role.MEMBERS)), ), # add sort order priority (first leader, manager and members) priority_role_order=Case( @@ -54,6 +50,7 @@ def queryset_users(role: GroupData.Role): When(pk__in=members, then=3), ), ) + .prefetch_related(skills_prefetch) .order_by("priority_role_order") ) diff --git a/apps/modules/project.py b/apps/modules/project.py index a4a283e4..30b7d750 100644 --- a/apps/modules/project.py +++ b/apps/modules/project.py @@ -41,10 +41,7 @@ def queryset_users(role: GroupData.Role): role=Case( When(pk__in=owners, then=Value(GroupData.Role.OWNERS)), When(pk__in=members, then=Value(GroupData.Role.MEMBERS)), - When( - pk__in=reviewers, - then=Value(GroupData.Role.REVIEWERS), - ), + When(pk__in=reviewers, then=Value(GroupData.Role.REVIEWERS)), ), # add sort order priority (first leader, manager and members) priority_role_order=Case( @@ -54,6 +51,7 @@ def queryset_users(role: GroupData.Role): ), ) .order_by("priority_role_order") + .distinct() ) def groups(self) -> QuerySet[PeopleGroup]: @@ -92,6 +90,7 @@ def queryset_groups(role: GroupData.Role): ), ) .order_by("priority_role_order") + .distinct() ) def linked_projects(self) -> QuerySet[Project]: From faddc40a155731ba9361f2ad0c11273cdc2e4f72 Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 24 Jun 2026 17:31:20 +0200 Subject: [PATCH 11/13] revert: permissions otpi --- apps/organizations/permissions.py | 8 ----- apps/projects/permissions.py | 57 ++++++++++++------------------- 2 files changed, 22 insertions(+), 43 deletions(-) diff --git a/apps/organizations/permissions.py b/apps/organizations/permissions.py index 68ecd4ed..dd762889 100644 --- a/apps/organizations/permissions.py +++ b/apps/organizations/permissions.py @@ -1,5 +1,3 @@ -from functools import cache - from django.db.models import Model from rest_framework import permissions from rest_framework.generics import get_object_or_404 @@ -20,12 +18,6 @@ class OrganizationRelatedPermission(IgnoreCall): - def __init__(self, *ar, **kw): - super().__init__(*ar, **kw) - - # add locale cache - self.get_related_organizations = cache(self.get_related_organizations) - def get_related_organizations( self, request: Request, view: GenericViewSet, obj: Model = None ) -> list[Organization]: diff --git a/apps/projects/permissions.py b/apps/projects/permissions.py index 4b03b51e..e002e91c 100644 --- a/apps/projects/permissions.py +++ b/apps/projects/permissions.py @@ -1,7 +1,4 @@ -from collections.abc import Generator from contextlib import suppress -from functools import cache -from typing import Any from django.db.models import Model from rest_framework import permissions @@ -19,12 +16,6 @@ class ProjectRelatedPermission(IgnoreCall): - def __init__(self, *ar, **kw): - super().__init__(*ar, **kw) - - # add locale cache - self.get_related_project = cache(self.get_related_project) - def get_related_project( self, request: Request, view: GenericViewSet, obj: Model | None = None ) -> Project | None: @@ -64,52 +55,48 @@ def HasProjectPermission( # noqa: N802 codename: str, app: str = "projects" ) -> permissions.BasePermission: class _HasProjectPermission(permissions.BasePermission, ProjectRelatedPermission): - def __init__(self, *ar, **kw): - super().__init__(*ar, **kw) - - self.has_permission = cache(self.has_permission) + def has_permission(self, request: Request, view: GenericViewSet) -> bool: + if request.user.is_authenticated: + project = self.get_related_project(request, view) + if project and app: + return request.user.has_perm(f"{app}.{codename}", project) + if project: + return request.user.has_perm(codename, project) + return False - def has_permission( - self, request: Request, view: GenericViewSet, project: Model = None + def has_object_permission( + self, request: Request, view: GenericViewSet, obj: Model ) -> bool: if request.user.is_authenticated: + project = self.get_related_project(request, view, obj) # If get_related_project returns None with a non-null obj, it might be # because the object is not yet linked to a project. In that case, it is # relevant to retry the permission check with the project_id in the URL. if not project: project = self.get_related_project(request, view) if project and app: - return request.user.has_perm(f"{app}.{codename}", project) + request.user.has_perm(f"{app}.{codename}", project) if project: return request.user.has_perm(codename, project) return False - def has_object_permission( - self, request: Request, view: GenericViewSet, obj: Model - ) -> bool: - return self.has_permission(request, view, obj) - return _HasProjectPermission class ProjectIsNotLocked(permissions.BasePermission, ProjectRelatedPermission): - def __init__(self, *ar, **kw): - super().__init__(*ar, **kw) - - self.cache_iter_perms = cache(self.user_can_modify_locked_project) - - def iter_perms( - self, project: Project, user: ProjectUser - ) -> Generator[tuple[bool], Any, Any]: - yield user.has_perm("projects.change_locked_project"), - yield user.has_perm("projects.change_locked_project", project), - for o in project.get_related_organizations(): - yield user.has_perm("organizations.change_locked_project", o) - def user_can_modify_locked_project( self, project: Project, user: ProjectUser ) -> bool: - return any(self.iter_perms(project, user)) + return any( + [ + user.has_perm("projects.change_locked_project"), + user.has_perm("projects.change_locked_project", project), + *[ + user.has_perm("organizations.change_locked_project", o) + for o in project.get_related_organizations() + ], + ] + ) def has_permission(self, request: Request, view: GenericViewSet) -> bool: return self.has_object_permission(request, view, None) From a7876f3fb26c99d34d926bb6799bf2d67a34c513 Mon Sep 17 00:00:00 2001 From: rgermain Date: Thu, 25 Jun 2026 10:37:06 +0200 Subject: [PATCH 12/13] fix: add timeout url --- apps/files/models.py | 14 ++++++++------ projects/settings/base.py | 5 +++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/files/models.py b/apps/files/models.py index d660df57..91590966 100644 --- a/apps/files/models.py +++ b/apps/files/models.py @@ -257,17 +257,19 @@ class Meta: @staticmethod def get_url(cache_key: str, field: ImageField) -> str: """create cache for url file""" - value = cache.get(cache_key) - if value: - return value + url = cache.get(cache_key) + if url: + return url try: - value = field.url + url = field.url except AttributeError: return "" - cache.set(cache_key, value) - return value + # expirations defined by azure/env - 60s + timeout = settings.STORAGE_EXPIRATION_SECS - 60 + cache.set(cache_key, url, timeout=timeout) + return url @property def __url_key(self) -> str: diff --git a/projects/settings/base.py b/projects/settings/base.py index eb163556..106e3f8c 100644 --- a/projects/settings/base.py +++ b/projects/settings/base.py @@ -387,6 +387,7 @@ # STORAGES # ############## +STORAGE_EXPIRATION_SECS = int(os.getenv("AZURE_URL_EXPIRATION_SECS", "3600")) STORAGES = { "default": { "BACKEND": os.getenv( @@ -396,8 +397,8 @@ "account_name": os.getenv("AZURE_ACCOUNT_NAME", "criparisdevlabprojects"), "account_key": os.getenv("AZURE_ACCOUNT_KEY", ""), "azure_container": os.getenv("AZURE_CONTAINER", "projects"), - "expiration_secs": int(os.getenv("AZURE_URL_EXPIRATION_SECS", "3600")), - "cache_control": f"private,max-age={os.getenv('AZURE_URL_EXPIRATION_SECS', '3600')},must-revalidate", + "expiration_secs": STORAGE_EXPIRATION_SECS, + "cache_control": f"private,max-age={STORAGE_EXPIRATION_SECS},must-revalidate", }, }, "staticfiles": { From bc69fe25777d4be6f64c4bbacc41529486d532d5 Mon Sep 17 00:00:00 2001 From: rgermain Date: Fri, 26 Jun 2026 10:05:20 +0200 Subject: [PATCH 13/13] optimize members/groups --- apps/modules/group.py | 70 ++++++++++++++------------ apps/modules/project.py | 108 +++++++++++++++++++++++----------------- 2 files changed, 100 insertions(+), 78 deletions(-) diff --git a/apps/modules/group.py b/apps/modules/group.py index 6f623fce..7af569ba 100644 --- a/apps/modules/group.py +++ b/apps/modules/group.py @@ -1,4 +1,12 @@ -from django.db.models import Case, Prefetch, Q, QuerySet, Value, When +from django.db.models import ( + Case, + CharField, + IntegerField, + Q, + QuerySet, + Value, + When, +) from apps.accounts.models import PeopleGroup, PeopleGroupLocation, ProjectUser from apps.commons.models import GroupData @@ -6,7 +14,6 @@ from apps.modules.base import AbstractModules, register_module from apps.newsfeed.models import Event, EventLocation, NewsLocation from apps.projects.models import Location, Project -from apps.skills.models import Skill from services.crisalid.models import Document, DocumentTypeCentralized @@ -15,43 +22,44 @@ class PeopleGroupModules(AbstractModules): instance: PeopleGroup def members(self) -> QuerySet[ProjectUser]: - def queryset_users(role: GroupData.Role): - group_data = GroupData.objects.filter( - role=role, group__people_groups=self.instance - ) - - return ProjectUser.objects.filter(groups__data__in=group_data) - - skills_prefetch = Prefetch( - "skills", queryset=Skill.objects.select_related("tag") - ) - - # get all members and annote rolepeople_groups - leaders = queryset_users(GroupData.Role.LEADERS) - managers = queryset_users(GroupData.Role.MANAGERS) - members = queryset_users(GroupData.Role.MEMBERS) - - # union all and filter by request.user - all_members = leaders | managers | members - return ( - all_members.distinct() - .filter(pk__in=self.user.get_user_queryset()) + self.user.get_user_queryset() + .filter( + groups__data__role__in=( + GroupData.Role.LEADERS, + GroupData.Role.MANAGERS, + GroupData.Role.MEMBERS, + ), + groups__people_groups=self.instance, + ) .annotate( role=Case( - When(pk__in=leaders, then=Value(GroupData.Role.LEADERS)), - When(pk__in=managers, then=Value(GroupData.Role.MANAGERS)), - When(pk__in=members, then=Value(GroupData.Role.MEMBERS)), + When( + groups__data__role=GroupData.Role.LEADERS, + then=Value(GroupData.Role.LEADERS.value), + ), + When( + groups__data__role=GroupData.Role.MANAGERS, + then=Value(GroupData.Role.MANAGERS.value), + ), + When( + groups__data__role=GroupData.Role.MEMBERS, + then=Value(GroupData.Role.MEMBERS.value), + ), + output_field=CharField(), ), - # add sort order priority (first leader, manager and members) priority_role_order=Case( - When(pk__in=leaders, then=1), - When(pk__in=managers, then=2), - When(pk__in=members, then=3), + When(groups__data__role=GroupData.Role.LEADERS, then=Value(1)), + When(groups__data__role=GroupData.Role.MANAGERS, then=Value(2)), + When( + groups__data__role=GroupData.Role.MEMBERS, + then=Value(3), + ), + output_field=IntegerField(), ), ) - .prefetch_related(skills_prefetch) .order_by("priority_role_order") + .distinct() ) def featured_projects(self) -> QuerySet[Project]: diff --git a/apps/modules/project.py b/apps/modules/project.py index 30b7d750..f9c4239a 100644 --- a/apps/modules/project.py +++ b/apps/modules/project.py @@ -1,4 +1,11 @@ -from django.db.models import Case, QuerySet, Value, When +from django.db.models import ( + Case, + CharField, + IntegerField, + QuerySet, + Value, + When, +) from apps.accounts.models import PeopleGroup, ProjectUser from apps.announcements.models import Announcement @@ -20,34 +27,37 @@ class ProjectModules(AbstractModules): instance: Project def members(self) -> QuerySet[ProjectUser]: - - def queryset_users(role: GroupData.Role): - group_data = GroupData.objects.filter( - role=role, group__projects=self.instance - ) - return ProjectUser.objects.filter(groups__data__in=group_data) - - # get all members and annote role - owners = queryset_users(GroupData.Role.OWNERS) - members = queryset_users(GroupData.Role.MEMBERS) - reviewers = queryset_users(GroupData.Role.REVIEWERS) - - # union all and filter by request.user - all_members = owners | members | reviewers return ( - all_members.distinct() - .filter(pk__in=self.user.get_user_queryset()) + self.user.get_user_queryset() + .filter( + groups__data__role__in=( + GroupData.Role.OWNERS, + GroupData.Role.MEMBERS, + GroupData.Role.REVIEWERS, + ), + groups__projects=self.instance, + ) .annotate( role=Case( - When(pk__in=owners, then=Value(GroupData.Role.OWNERS)), - When(pk__in=members, then=Value(GroupData.Role.MEMBERS)), - When(pk__in=reviewers, then=Value(GroupData.Role.REVIEWERS)), + When( + groups__data__role=GroupData.Role.OWNERS, + then=Value(GroupData.Role.OWNERS.value), + ), + When( + groups__data__role=GroupData.Role.MEMBERS, + then=Value(GroupData.Role.MEMBERS.value), + ), + When( + groups__data__role=GroupData.Role.REVIEWERS, + then=Value(GroupData.Role.REVIEWERS.value), + ), + output_field=CharField(), ), - # add sort order priority (first leader, manager and members) priority_role_order=Case( - When(pk__in=owners, then=1), - When(pk__in=members, then=2), - When(pk__in=reviewers, then=3), + When(groups__data__role=GroupData.Role.OWNERS, then=Value(1)), + When(groups__data__role=GroupData.Role.MEMBERS, then=Value(2)), + When(groups__data__role=GroupData.Role.REVIEWERS, then=Value(3)), + output_field=IntegerField(), ), ) .order_by("priority_role_order") @@ -55,38 +65,42 @@ def queryset_users(role: GroupData.Role): ) def groups(self) -> QuerySet[PeopleGroup]: - def queryset_groups(role: GroupData.Role): - group_data = GroupData.objects.filter( - role=role, group__projects=self.instance - ) - return PeopleGroup.objects.filter(groups__data__in=group_data) - - # get all members and annote role - owner_groups = queryset_groups(GroupData.Role.OWNER_GROUPS) - member_groups = queryset_groups(GroupData.Role.MEMBER_GROUPS) - reviewer_groups = queryset_groups(GroupData.Role.REVIEWER_GROUPS) - - # union all and filter by request.user - all_groups = owner_groups | member_groups | reviewer_groups return ( - all_groups.distinct() - .filter(pk__in=self.user.get_people_group_queryset()) + self.user.get_people_group_queryset() + .filter( + groups__data__role__in=( + GroupData.Role.OWNER_GROUPS, + GroupData.Role.MEMBER_GROUPS, + GroupData.Role.REVIEWER_GROUPS, + ), + groups__projects=self.instance, + ) .annotate( role=Case( - When(pk__in=owner_groups, then=Value(GroupData.Role.OWNER_GROUPS)), When( - pk__in=member_groups, then=Value(GroupData.Role.MEMBER_GROUPS) + groups__data__role=GroupData.Role.OWNER_GROUPS, + then=Value(GroupData.Role.OWNER_GROUPS.value), ), When( - pk__in=reviewer_groups, - then=Value(GroupData.Role.REVIEWER_GROUPS), + groups__data__role=GroupData.Role.MEMBER_GROUPS, + then=Value(GroupData.Role.MEMBER_GROUPS.value), ), + When( + groups__data__role=GroupData.Role.REVIEWER_GROUPS, + then=Value(GroupData.Role.REVIEWER_GROUPS.value), + ), + output_field=CharField(), ), - # add sort order priority (first leader, manager and members) priority_role_order=Case( - When(pk__in=owner_groups, then=1), - When(pk__in=member_groups, then=2), - When(pk__in=reviewer_groups, then=3), + When(groups__data__role=GroupData.Role.OWNER_GROUPS, then=Value(1)), + When( + groups__data__role=GroupData.Role.MEMBER_GROUPS, then=Value(2) + ), + When( + groups__data__role=GroupData.Role.REVIEWER_GROUPS, + then=Value(3), + ), + output_field=IntegerField(), ), ) .order_by("priority_role_order")