From 417643fa8c9875a2280902d07bf26a06f6201f5d Mon Sep 17 00:00:00 2001 From: rgermain Date: Tue, 17 Mar 2026 18:43:06 +0100 Subject: [PATCH 1/5] fix: slug or id group --- apps/accounts/models.py | 3 +++ apps/commons/queryset.py | 22 +++++++++++++++++++ .../tests/views/test_organization.py | 17 ++++++++++++++ apps/organizations/views.py | 9 ++++---- 4 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 apps/commons/queryset.py diff --git a/apps/accounts/models.py b/apps/accounts/models.py index c302d2e7..b7e0434e 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -34,6 +34,7 @@ OrganizationRelated, ) from apps.commons.models import GroupData +from apps.commons.queryset import MultipleIdQuerySet from apps.newsfeed.models import Event, Instruction, News from apps.organizations.models import Organization from apps.projects.models import AbstractLocation, Project @@ -99,6 +100,8 @@ class PeopleGroup( The visibility setting of the group. """ + objects = MultipleIdQuerySet.as_manager() + auto_translated_fields: list[str] = [ "name", "html:description", diff --git a/apps/commons/queryset.py b/apps/commons/queryset.py new file mode 100644 index 00000000..765f9c7e --- /dev/null +++ b/apps/commons/queryset.py @@ -0,0 +1,22 @@ +from collections import defaultdict +from typing import Self + +from django.db import models + + +class MultipleIdQuerySet(models.QuerySet): + """queryset/manager to filter queryset by id or slug""" + + def _build_identifiers_query(self, identifiers: tuple[str | int]): + query = defaultdict(list) + for iden in identifiers: + field = self.model.get_id_field_name(iden) + query[field].append(iden) + + return {f"{field}__in": ids for field, ids in query.items()} + + def slug_or_id(self, identifier: str | int) -> Self: + return self.filter(**self._build_identifiers_query((identifier,))) + + def slug_or_ids(self, identifiers: tuple[str | int]) -> Self: + return self.filter(**self._build_identifiers_query(identifiers)) diff --git a/apps/organizations/tests/views/test_organization.py b/apps/organizations/tests/views/test_organization.py index 0828f1cf..a86625d3 100644 --- a/apps/organizations/tests/views/test_organization.py +++ b/apps/organizations/tests/views/test_organization.py @@ -780,6 +780,23 @@ def test_people_groups_hierarchy_params(self): self.assertEqual(len(data["hierarchy"]), 1) self.assertEqual(data["hierarchy"][0]["name"], self.root_group.name) + def test_people_groups_hierarchy_slug_or_id(self): + organization = self.organization + user = self.get_parameterized_test_user(TestRoles.SUPERADMIN) + self.client.force_authenticate(user) + + url = reverse( + "Organization-people-groups-hierarchy", + args=(organization.code,), + ) + parent = self.root_group.children.all().first() + response = self.client.get(url, {"depth": 0, "parent": parent.id}) + results_id = response.json() + response = self.client.get(url, {"depth": 0, "parent": parent.slug}) + results_slug = response.json() + + self.assertDictEqual(results_id, results_slug) + class ValidateOrganizationTestCase(JwtAPITestCase): @classmethod diff --git a/apps/organizations/views.py b/apps/organizations/views.py index 3ed76fc3..d654f567 100644 --- a/apps/organizations/views.py +++ b/apps/organizations/views.py @@ -372,7 +372,7 @@ def remove_member(self, request, *args, **kwargs): name="parent", description="people group parents to serializer", required=False, - type=int, + type=int | str, ), PeopleGroupModules.ApiParameter(), ], @@ -392,10 +392,9 @@ def get_people_groups_hierarchy(self, request, *args, **kwargs): # get root "organization" group if parent is not set if request.query_params.get("parent"): root_group = get_object_or_404( - request.user.get_people_group_queryset().filter( - organization=organization - ), - pk=request.query_params.get("parent"), + request.user.get_people_group_queryset() + .filter(organization=organization) + .slug_or_id(request.query_params.get("parent")) ) else: root_group = PeopleGroup.update_or_create_root(organization) From f304201a5db37ed21491c77b90fcc5810e9fd96b Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 18 Mar 2026 10:17:55 +0100 Subject: [PATCH 2/5] fix: 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 c5764688..6cea30d1 100644 --- a/locale/ca/LC_MESSAGES/django.po +++ b/locale/ca/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-05 20:01+0100\n" +"POT-Creation-Date: 2026-03-18 10:17+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:158 apps/projects/models.py:164 +#: apps/accounts/models.py:161 apps/projects/models.py:164 msgid "visibility" msgstr "visibilitat" diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index 6df26b7e..b57f3fd4 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-05 20:01+0100\n" +"POT-Creation-Date: 2026-03-18 10:17+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:158 apps/projects/models.py:164 +#: apps/accounts/models.py:161 apps/projects/models.py:164 msgid "visibility" msgstr "Sichtbarkeit" diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index 8d43261d..de65f8d1 100644 --- a/locale/en/LC_MESSAGES/django.po +++ b/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-05 20:01+0100\n" +"POT-Creation-Date: 2026-03-18 10:17+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:158 apps/projects/models.py:164 +#: apps/accounts/models.py:161 apps/projects/models.py:164 msgid "visibility" msgstr "" diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po index 5bff55b3..6f6c5efb 100644 --- a/locale/es/LC_MESSAGES/django.po +++ b/locale/es/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-05 20:01+0100\n" +"POT-Creation-Date: 2026-03-18 10:17+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:158 apps/projects/models.py:164 +#: apps/accounts/models.py:161 apps/projects/models.py:164 msgid "visibility" msgstr "visibilidad" diff --git a/locale/et/LC_MESSAGES/django.po b/locale/et/LC_MESSAGES/django.po index b6b95bd8..3f34af90 100644 --- a/locale/et/LC_MESSAGES/django.po +++ b/locale/et/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-05 20:01+0100\n" +"POT-Creation-Date: 2026-03-18 10:17+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:158 apps/projects/models.py:164 +#: apps/accounts/models.py:161 apps/projects/models.py:164 msgid "visibility" msgstr "nähtavus" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index e74d24a8..cc5805df 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-05 20:01+0100\n" +"POT-Creation-Date: 2026-03-18 10:17+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:158 apps/projects/models.py:164 +#: apps/accounts/models.py:161 apps/projects/models.py:164 msgid "visibility" msgstr "visibilité" diff --git a/locale/nl/LC_MESSAGES/django.po b/locale/nl/LC_MESSAGES/django.po index 367814e0..c162123c 100644 --- a/locale/nl/LC_MESSAGES/django.po +++ b/locale/nl/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-05 20:01+0100\n" +"POT-Creation-Date: 2026-03-18 10:17+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:158 apps/projects/models.py:164 +#: apps/accounts/models.py:161 apps/projects/models.py:164 msgid "visibility" msgstr "zichtbaarheid" From 08e87973c4844bfd7e73606079a7285e679759c8 Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 18 Mar 2026 11:39:35 +0100 Subject: [PATCH 3/5] fix: mutliIdQueryset --- apps/accounts/models.py | 2 ++ apps/commons/queryset.py | 39 +++++++++++++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/apps/accounts/models.py b/apps/accounts/models.py index b7e0434e..944c8c52 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -293,6 +293,8 @@ class ProjectUser( Override Django base user by a user of projects app """ + objects = MultipleIdQuerySet.as_manager() + organization_query_string: str = "groups__organizations" auto_translated_fields: list[str] = [ "html:description", diff --git a/apps/commons/queryset.py b/apps/commons/queryset.py index 765f9c7e..c124d3e6 100644 --- a/apps/commons/queryset.py +++ b/apps/commons/queryset.py @@ -1,22 +1,49 @@ from collections import defaultdict from typing import Self +from django.contrib.postgres.fields import ArrayField from django.db import models class MultipleIdQuerySet(models.QuerySet): """queryset/manager to filter queryset by id or slug""" - def _build_identifiers_query(self, identifiers: tuple[str | int]): - query = defaultdict(list) + def _related_field(self, model: models.Model, field: str) -> models.Field: + """traverse fields query to get real field model""" + + acutal_model = model + *traverse, last = field.split("__") + for sub in traverse: + acutal_model = acutal_model._meta.get_field(sub).related_model + return acutal_model._meta.get_field(last) + + def build_identifiers_query(self, identifiers: tuple[str | int]) -> models.Q: + query = defaultdict(set) + for iden in identifiers: field = self.model.get_id_field_name(iden) - query[field].append(iden) + if field == "slug" and hasattr(self.model, "outdated_slugs"): + fields = (field, "outdated_slugs") + else: + fields = (field,) + + for field in fields: + query[field].add(str(iden)) + + fianl_query = models.Q() + for field, values in query.items(): + field_cls = self._related_field(self.model, field) + if isinstance(field_cls, ArrayField): + lookup = "__contains" + else: + lookup = "__in" + + fianl_query |= models.Q(**{f"{field}{lookup}": list(values)}) - return {f"{field}__in": ids for field, ids in query.items()} + return fianl_query def slug_or_id(self, identifier: str | int) -> Self: - return self.filter(**self._build_identifiers_query((identifier,))) + return self.filter(self.build_identifiers_query((identifier,))) def slug_or_ids(self, identifiers: tuple[str | int]) -> Self: - return self.filter(**self._build_identifiers_query(identifiers)) + return self.filter(self.build_identifiers_query(identifiers)) From 3a0f0baf15484363169fc9b9d827577f69c9cb70 Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 18 Mar 2026 14:00:37 +0100 Subject: [PATCH 4/5] fix: manager --- apps/accounts/models.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/accounts/models.py b/apps/accounts/models.py index 944c8c52..b7e0434e 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -293,8 +293,6 @@ class ProjectUser( Override Django base user by a user of projects app """ - objects = MultipleIdQuerySet.as_manager() - organization_query_string: str = "groups__organizations" auto_translated_fields: list[str] = [ "html:description", From 8651a6a3a9f39d9ca1211ab7a6ab02a0b0ada1e5 Mon Sep 17 00:00:00 2001 From: Sam Onaisi Date: Wed, 18 Mar 2026 16:41:42 +0100 Subject: [PATCH 5/5] naming changes --- apps/accounts/models.py | 4 ++-- apps/commons/queryset.py | 18 +++++++++--------- .../tests/views/test_organization.py | 5 +++++ 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/apps/accounts/models.py b/apps/accounts/models.py index b7e0434e..5d54fba7 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -34,7 +34,7 @@ OrganizationRelated, ) from apps.commons.models import GroupData -from apps.commons.queryset import MultipleIdQuerySet +from apps.commons.queryset import MultipleIdsQuerySet from apps.newsfeed.models import Event, Instruction, News from apps.organizations.models import Organization from apps.projects.models import AbstractLocation, Project @@ -100,7 +100,7 @@ class PeopleGroup( The visibility setting of the group. """ - objects = MultipleIdQuerySet.as_manager() + objects = MultipleIdsQuerySet.as_manager() auto_translated_fields: list[str] = [ "name", diff --git a/apps/commons/queryset.py b/apps/commons/queryset.py index c124d3e6..81e94c81 100644 --- a/apps/commons/queryset.py +++ b/apps/commons/queryset.py @@ -5,10 +5,10 @@ from django.db import models -class MultipleIdQuerySet(models.QuerySet): +class MultipleIdsQuerySet(models.QuerySet): """queryset/manager to filter queryset by id or slug""" - def _related_field(self, model: models.Model, field: str) -> models.Field: + def _get_related_field(self, model: models.Model, field: str) -> models.Field: """traverse fields query to get real field model""" acutal_model = model @@ -20,27 +20,27 @@ def _related_field(self, model: models.Model, field: str) -> models.Field: def build_identifiers_query(self, identifiers: tuple[str | int]) -> models.Q: query = defaultdict(set) - for iden in identifiers: - field = self.model.get_id_field_name(iden) + for identifier in identifiers: + field = self.model.get_id_field_name(identifier) if field == "slug" and hasattr(self.model, "outdated_slugs"): fields = (field, "outdated_slugs") else: fields = (field,) for field in fields: - query[field].add(str(iden)) + query[field].add(str(identifier)) - fianl_query = models.Q() + final_query = models.Q() for field, values in query.items(): - field_cls = self._related_field(self.model, field) + field_cls = self._get_related_field(self.model, field) if isinstance(field_cls, ArrayField): lookup = "__contains" else: lookup = "__in" - fianl_query |= models.Q(**{f"{field}{lookup}": list(values)}) + final_query |= models.Q(**{f"{field}{lookup}": list(values)}) - return fianl_query + return final_query def slug_or_id(self, identifier: str | int) -> Self: return self.filter(self.build_identifiers_query((identifier,))) diff --git a/apps/organizations/tests/views/test_organization.py b/apps/organizations/tests/views/test_organization.py index a86625d3..d004da11 100644 --- a/apps/organizations/tests/views/test_organization.py +++ b/apps/organizations/tests/views/test_organization.py @@ -790,12 +790,17 @@ def test_people_groups_hierarchy_slug_or_id(self): args=(organization.code,), ) parent = self.root_group.children.all().first() + parent.outdated_slugs = [f"{parent.slug}-old"] + parent.save() response = self.client.get(url, {"depth": 0, "parent": parent.id}) results_id = response.json() response = self.client.get(url, {"depth": 0, "parent": parent.slug}) results_slug = response.json() + response = self.client.get(url, {"depth": 0, "parent": f"{parent.slug}-old"}) + results_outdated_slug = response.json() self.assertDictEqual(results_id, results_slug) + self.assertDictEqual(results_id, results_outdated_slug) class ValidateOrganizationTestCase(JwtAPITestCase):