From e8a449c644231d542687ef9baee8ab451acc4fe2 Mon Sep 17 00:00:00 2001 From: rgermain Date: Tue, 13 Jan 2026 17:37:52 +0100 Subject: [PATCH 1/3] feat: add modules groups --- apps/accounts/models.py | 2 ++ apps/accounts/serializers.py | 17 +++++++++-- apps/accounts/views.py | 50 +++++-------------------------- apps/commons/mixins.py | 9 ++++++ apps/modules/__init__.py | 3 ++ apps/modules/base.py | 49 +++++++++++++++++++++++++++++++ apps/modules/group.py | 57 ++++++++++++++++++++++++++++++++++++ 7 files changed, 143 insertions(+), 44 deletions(-) create mode 100644 apps/modules/__init__.py create mode 100644 apps/modules/base.py create mode 100644 apps/modules/group.py diff --git a/apps/accounts/models.py b/apps/accounts/models.py index 0fbb1fd4..0fba032d 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -25,6 +25,7 @@ ) from apps.commons.enums import SDG, Language from apps.commons.mixins import ( + HasModulesRelated, HasMultipleIDs, HasOwner, HasPermissionsSetup, @@ -41,6 +42,7 @@ class PeopleGroup( + HasModulesRelated, HasAutoTranslatedFields, HasMultipleIDs, HasPermissionsSetup, diff --git a/apps/accounts/serializers.py b/apps/accounts/serializers.py index 41e79e68..c4abccb4 100644 --- a/apps/accounts/serializers.py +++ b/apps/accounts/serializers.py @@ -237,6 +237,16 @@ class Meta: fields = read_only_fields +class ModulesSerializers(serializers.ModelSerializer): + modules = serializers.SerializerMethodField() + + def get_modules(self, instance): + request = self.context.get("request") + + cls = instance.get_related_module() + return cls(instance, user=request.user).count() + + class PeopleGroupLightSerializer( AutoTranslatedModelSerializer, serializers.ModelSerializer ): @@ -402,7 +412,10 @@ def create(self, validated_data): class PeopleGroupSerializer( - StringsImagesSerializer, AutoTranslatedModelSerializer, serializers.ModelSerializer + ModulesSerializers, + StringsImagesSerializer, + AutoTranslatedModelSerializer, + serializers.ModelSerializer, ): string_images_forbid_fields: List[str] = [ @@ -527,7 +540,7 @@ def save(self, **kwargs): class Meta: model = PeopleGroup - read_only_fields = ["is_root", "slug"] + read_only_fields = ["is_root", "slug", "modules"] fields = read_only_fields + [ "id", "name", diff --git a/apps/accounts/views.py b/apps/accounts/views.py index f537d027..0e89a99c 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -40,7 +40,6 @@ from apps.files.views import ImageStorageView from apps.organizations.models import Organization from apps.organizations.permissions import HasOrganizationPermission -from apps.projects.models import Project from apps.projects.serializers import ProjectLightSerializer from apps.skills.models import Skill from services.google.models import GoogleAccount, GoogleGroup @@ -682,27 +681,10 @@ def remove_member(self, request, *args, **kwargs): ) def member(self, request, *args, **kwargs): group = self.get_object() - managers_ids = group.managers.all().values_list("id", flat=True) - leaders_ids = group.leaders.all().values_list("id", flat=True) - skills_prefetch = Prefetch( - "skills", queryset=Skill.objects.select_related("tag") - ) - queryset = ( - group.get_all_members() - .distinct() - .annotate( - is_leader=Case( - When(id__in=leaders_ids, then=True), default=Value(False) - ) - ) - .annotate( - is_manager=Case( - When(id__in=managers_ids, then=True), default=Value(False) - ) - ) - .order_by("-is_leader", "-is_manager") - .prefetch_related(skills_prefetch, "groups") - ) + + cls = group.get_related_module() + module = cls(group, request.user) + queryset = module.members() page = self.paginate_queryset(queryset) if page is not None: @@ -790,26 +772,10 @@ def remove_featured_project(self, request, *args, **kwargs): ) def project(self, request, *args, **kwargs): group = self.get_object() - group_projects_ids = ( - Project.objects.filter(groups__people_groups=group) - .distinct() - .values_list("id", flat=True) - ) - queryset = ( - self.request.user.get_project_queryset() - .filter(Q(groups__people_groups=group) | Q(people_groups=group)) - .annotate( - is_group_project=Case( - When(id__in=group_projects_ids, then=True), default=Value(False) - ), - is_featured=Case( - When(people_groups=group, then=True), default=Value(False) - ), - ) - .distinct() - .order_by("-is_featured", "-is_group_project") - .prefetch_related("categories") - ) + cls = group.get_related_module() + module = cls(group, request.user) + queryset = module.featured_projects() + page = self.paginate_queryset(queryset) if page is not None: project_serializer = ProjectLightSerializer( diff --git a/apps/commons/mixins.py b/apps/commons/mixins.py index 84ef04d5..c31f93bc 100644 --- a/apps/commons/mixins.py +++ b/apps/commons/mixins.py @@ -408,3 +408,12 @@ def get_slug(self) -> str: if self.get_id_field_name(slug) != "slug": slug = f"{self.slug_prefix}-{slug}" return slug + + +class HasModulesRelated: + """Mixins for related modules class""" + + def get_related_module(self): + from apps.modules.base import get_module + + return get_module(type(self)) diff --git a/apps/modules/__init__.py b/apps/modules/__init__.py new file mode 100644 index 00000000..98a20100 --- /dev/null +++ b/apps/modules/__init__.py @@ -0,0 +1,3 @@ +from .group import PeopleGroupModules + +__all__ = ["PeopleGroupModules"] diff --git a/apps/modules/base.py b/apps/modules/base.py new file mode 100644 index 00000000..679efe76 --- /dev/null +++ b/apps/modules/base.py @@ -0,0 +1,49 @@ +import inspect + +from django.db import models + + +class AbstractModules: + """abstract class for modules/queryset declarations""" + + def __init__(self, instance, /, user, **kw): + self.instance = instance + self.user = user + + def count(self): + members = inspect.getmembers( + self, + predicate=inspect.ismethod, + ) + + modules = {} + for name, func in members: + # ignore private_method and "count" method (this method :D) + if name.startswith("_") or name in ("count",): + continue + + # func return queryset + modules[name] = func().count() + + return modules + + +_modules: dict[models.Model] = {} + + +def register_module(model: models.Model): + """decorator to register modules assoiate on models + + :param model: _description_ + """ + + def _wrap(cls): + _modules[model] = cls + return cls + + return _wrap + + +def get_module(model: models.Model): + """get regisered module""" + return _modules[model] diff --git a/apps/modules/group.py b/apps/modules/group.py new file mode 100644 index 00000000..7ad52d6c --- /dev/null +++ b/apps/modules/group.py @@ -0,0 +1,57 @@ +from django.db.models import Case, Prefetch, Q, QuerySet, Value, When + +from apps.accounts.models import PeopleGroup, ProjectUser +from apps.modules.base import AbstractModules, register_module +from apps.projects.models import Project +from apps.skills.models import Skill + + +@register_module(PeopleGroup) +class PeopleGroupModules(AbstractModules): + def members(self) -> QuerySet[ProjectUser]: + managers_ids = self.instance.managers.all().values_list("id", flat=True) + leaders_ids = self.instance.leaders.all().values_list("id", flat=True) + skills_prefetch = Prefetch( + "skills", queryset=Skill.objects.select_related("tag") + ) + return ( + self.instance.get_all_members() + .distinct() + .annotate( + is_leader=Case( + When(id__in=leaders_ids, then=True), default=Value(False) + ) + ) + .annotate( + is_manager=Case( + When(id__in=managers_ids, then=True), default=Value(False) + ) + ) + .order_by("-is_leader", "-is_manager") + .prefetch_related(skills_prefetch, "groups") + ) + + def featured_projects(self) -> QuerySet[Project]: + group_projects_ids = ( + Project.objects.filter(groups__people_groups=self.instance) + .distinct() + .values_list("id", flat=True) + ) + + return ( + self.user.get_project_queryset() + .filter( + Q(groups__people_groups=self.instance) | Q(people_groups=self.instance) + ) + .annotate( + is_group_project=Case( + When(id__in=group_projects_ids, then=True), default=Value(False) + ), + is_featured=Case( + When(people_groups=self.instance, then=True), default=Value(False) + ), + ) + .distinct() + .order_by("-is_featured", "-is_group_project") + .prefetch_related("categories") + ) From f9c489f11e36ecdb3b658023a6a9d84ab27bd6bd Mon Sep 17 00:00:00 2001 From: rgermain Date: Tue, 13 Jan 2026 18:03:28 +0100 Subject: [PATCH 2/3] fix: i18n messages --- locale/ca/LC_MESSAGES/django.po | 4 ++-- locale/de/LC_MESSAGES/django.po | 4 ++-- locale/en/LC_MESSAGES/django.po | 4 ++-- locale/es/LC_MESSAGES/django.po | 4 ++-- locale/et/LC_MESSAGES/django.po | 4 ++-- locale/fr/LC_MESSAGES/django.po | 4 ++-- locale/nl/LC_MESSAGES/django.po | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/locale/ca/LC_MESSAGES/django.po b/locale/ca/LC_MESSAGES/django.po index 2daf0109..0963381d 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: 2025-12-17 16:58+0100\n" +"POT-Creation-Date: 2026-01-13 18:03+0100\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:140 apps/projects/models.py:161 +#: apps/accounts/models.py:142 apps/projects/models.py:161 msgid "visibility" msgstr "visibilitat" diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index da18dcdb..726ab799 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: 2025-12-17 16:58+0100\n" +"POT-Creation-Date: 2026-01-13 18:03+0100\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:140 apps/projects/models.py:161 +#: apps/accounts/models.py:142 apps/projects/models.py:161 msgid "visibility" msgstr "Sichtbarkeit" diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index dc59aeda..bc643b3d 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: 2025-12-17 16:58+0100\n" +"POT-Creation-Date: 2026-01-13 18:03+0100\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:140 apps/projects/models.py:161 +#: apps/accounts/models.py:142 apps/projects/models.py:161 msgid "visibility" msgstr "" diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po index ddb05f29..e1390992 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: 2025-12-17 16:58+0100\n" +"POT-Creation-Date: 2026-01-13 18:03+0100\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:140 apps/projects/models.py:161 +#: apps/accounts/models.py:142 apps/projects/models.py:161 msgid "visibility" msgstr "visibilidad" diff --git a/locale/et/LC_MESSAGES/django.po b/locale/et/LC_MESSAGES/django.po index 29bd2ce7..f5924a85 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: 2025-12-17 16:58+0100\n" +"POT-Creation-Date: 2026-01-13 18:03+0100\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:140 apps/projects/models.py:161 +#: apps/accounts/models.py:142 apps/projects/models.py:161 msgid "visibility" msgstr "nähtavus" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index cd3ed07a..a39d7ed2 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: 2025-12-17 16:58+0100\n" +"POT-Creation-Date: 2026-01-13 18:03+0100\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:140 apps/projects/models.py:161 +#: apps/accounts/models.py:142 apps/projects/models.py:161 msgid "visibility" msgstr "visibilité" diff --git a/locale/nl/LC_MESSAGES/django.po b/locale/nl/LC_MESSAGES/django.po index 886b70c0..5287acd7 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: 2025-12-17 16:58+0100\n" +"POT-Creation-Date: 2026-01-13 18:03+0100\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:140 apps/projects/models.py:161 +#: apps/accounts/models.py:142 apps/projects/models.py:161 msgid "visibility" msgstr "zichtbaarheid" From be727aaa8d8cd8dd94928cb86e63aafaa5dbae9d Mon Sep 17 00:00:00 2001 From: rgermain Date: Thu, 15 Jan 2026 09:34:08 +0100 Subject: [PATCH 3/3] fix: serializers import --- apps/accounts/serializers.py | 13 ++----------- apps/accounts/views.py | 12 ++++++------ apps/commons/serializers.py | 12 ++++++++++++ 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/apps/accounts/serializers.py b/apps/accounts/serializers.py index c4abccb4..6962a24f 100644 --- a/apps/accounts/serializers.py +++ b/apps/accounts/serializers.py @@ -16,7 +16,7 @@ ) from apps.commons.mixins import HasPermissionsSetup from apps.commons.models import GroupData -from apps.commons.serializers import StringsImagesSerializer +from apps.commons.serializers import ModulesSerializers, StringsImagesSerializer from apps.files.models import Image from apps.files.serializers import ImageSerializer from apps.notifications.models import Notification @@ -237,16 +237,6 @@ class Meta: fields = read_only_fields -class ModulesSerializers(serializers.ModelSerializer): - modules = serializers.SerializerMethodField() - - def get_modules(self, instance): - request = self.context.get("request") - - cls = instance.get_related_module() - return cls(instance, user=request.user).count() - - class PeopleGroupLightSerializer( AutoTranslatedModelSerializer, serializers.ModelSerializer ): @@ -260,6 +250,7 @@ class PeopleGroupLightSerializer( ) organization = serializers.SlugRelatedField(read_only=True, slug_field="code") + # TODO(remi): replace this by modules def get_members_count(self, group: PeopleGroup) -> int: return group.get_all_members().count() diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 0e89a99c..e31b2e22 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -682,9 +682,9 @@ def remove_member(self, request, *args, **kwargs): def member(self, request, *args, **kwargs): group = self.get_object() - cls = group.get_related_module() - module = cls(group, request.user) - queryset = module.members() + modules_manager = group.get_related_module() + modules = modules_manager(group, request.user) + queryset = modules.members() page = self.paginate_queryset(queryset) if page is not None: @@ -772,9 +772,9 @@ def remove_featured_project(self, request, *args, **kwargs): ) def project(self, request, *args, **kwargs): group = self.get_object() - cls = group.get_related_module() - module = cls(group, request.user) - queryset = module.featured_projects() + modules_manager = group.get_related_module() + modules = modules_manager(group, request.user) + queryset = modules.featured_projects() page = self.paginate_queryset(queryset) if page is not None: diff --git a/apps/commons/serializers.py b/apps/commons/serializers.py index c6679a7a..048fb80a 100644 --- a/apps/commons/serializers.py +++ b/apps/commons/serializers.py @@ -221,3 +221,15 @@ def save(self, **kwargs): return self.instance instance = super().save(**kwargs) return self.add_string_images_to_instance(instance, images) + + +class ModulesSerializers(serializers.ModelSerializer): + """Modules serializers to return how many elements is linked to objects""" + + modules = serializers.SerializerMethodField() + + def get_modules(self, instance): + request = self.context.get("request") + + modules_manager = instance.get_related_module() + return modules_manager(instance, user=request.user).count()