Skip to content

Commit bcf2c42

Browse files
authored
feat(hierarchies): Recursive serializers (#468)
* Recursive serializers * fix root hierarchies children with no parents
1 parent 61c7ab4 commit bcf2c42

File tree

13 files changed

+178
-114
lines changed

13 files changed

+178
-114
lines changed

apps/accounts/models.py

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
from django.contrib.auth.models import AbstractUser, Group, Permission
88
from django.contrib.contenttypes.models import ContentType
9-
from django.contrib.postgres.aggregates import ArrayAgg
109
from django.contrib.postgres.fields import ArrayField
1110
from django.core.validators import MaxValueValidator
1211
from django.db import models, transaction
@@ -177,43 +176,6 @@ def update_or_create_root(cls, organization: "Organization"):
177176
)
178177
return root_group
179178

180-
@classmethod
181-
def _get_hierarchy(cls, groups: dict[int, dict], group_id: int):
182-
from apps.files.serializers import ImageSerializer
183-
184-
return {
185-
"id": groups[group_id].id,
186-
"slug": groups[group_id].slug,
187-
"name": groups[group_id].name,
188-
"publication_status": groups[group_id].publication_status,
189-
"children": [
190-
cls._get_hierarchy(groups, child)
191-
for child in groups[group_id].children_ids
192-
if child is not None and child in groups
193-
],
194-
"roles": [group.name for group in groups[group_id].groups.all()],
195-
"header_image": (
196-
ImageSerializer(groups[group_id].header_image).data
197-
if groups[group_id].header_image
198-
else None
199-
),
200-
}
201-
202-
def get_hierarchy(self, user: Optional["ProjectUser"] = None) -> dict:
203-
# This would be better with a recursive serializer, but it doubles the query time
204-
if user:
205-
groups = (
206-
user.get_people_group_queryset()
207-
| PeopleGroup.objects.filter(is_root=True).distinct()
208-
)
209-
else:
210-
groups = PeopleGroup.objects.all()
211-
groups = groups.filter(organization=self.organization.pk).annotate(
212-
children_ids=ArrayAgg("children")
213-
)
214-
groups = {group.id: group for group in groups}
215-
return self._get_hierarchy(groups, self.id)
216-
217179
def get_default_managers_permissions(self) -> QuerySet[Permission]:
218180
return Permission.objects.filter(content_type=self.content_type)
219181

apps/accounts/serializers.py

Lines changed: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,11 @@ class Meta:
8282

8383
def get_people_groups(self, user: ProjectUser) -> list:
8484
organization = self.context.get("organization")
85-
queryset = PeopleGroup.objects.filter(
86-
groups__users=user, is_root=False
87-
).distinct()
85+
queryset = (
86+
PeopleGroup.objects.filter(groups__users=user, is_root=False)
87+
.select_related("organization")
88+
.distinct()
89+
)
8890
if organization:
8991
queryset = queryset.filter(organization=organization).distinct()
9092
return PeopleGroupSuperLightSerializer(
@@ -199,6 +201,7 @@ def get_people_groups(self, user: ProjectUser) -> list:
199201
queryset = (
200202
request_user.get_people_group_queryset()
201203
.filter(groups__users=user, is_root=False)
204+
.select_related("organization")
202205
.distinct()
203206
)
204207
if organization:
@@ -226,9 +229,11 @@ def get_can_mentor_on(self, user: ProjectUser) -> List[Dict]:
226229
class PeopleGroupSuperLightSerializer(
227230
AutoTranslatedModelSerializer, serializers.ModelSerializer
228231
):
232+
organization = serializers.SlugRelatedField(read_only=True, slug_field="code")
233+
229234
class Meta:
230235
model = PeopleGroup
231-
read_only_fields = ["id", "slug", "name"]
236+
read_only_fields = ["id", "slug", "name", "organization"]
232237
fields = read_only_fields
233238

234239

@@ -264,6 +269,57 @@ class Meta:
264269
]
265270

266271

272+
class PeopleGroupHierarchySerializer(
273+
AutoTranslatedModelSerializer,
274+
serializers.ModelSerializer,
275+
):
276+
children = serializers.SerializerMethodField()
277+
header_image = ImageSerializer(read_only=True)
278+
roles = serializers.SlugRelatedField(
279+
many=True,
280+
slug_field="name",
281+
read_only=True,
282+
source="groups",
283+
)
284+
285+
class Meta:
286+
model = PeopleGroup
287+
read_only_fields = [
288+
"id",
289+
"slug",
290+
"name",
291+
"publication_status",
292+
"header_image",
293+
"children",
294+
"roles",
295+
]
296+
fields = read_only_fields
297+
298+
def get_children(
299+
self, people_group: PeopleGroup
300+
) -> List[Dict[str, Union[str, int]]]:
301+
context = self.context
302+
request = context.get("request")
303+
mapping = context.get("mapping")
304+
if not mapping:
305+
base_queryset = request.user.get_people_group_queryset().filter(
306+
organization=people_group.organization
307+
)
308+
mapping = {group.id: group for group in base_queryset}
309+
context["mapping"] = mapping
310+
children_ids = list(people_group.children.all().values_list("id", flat=True))
311+
if people_group.is_root:
312+
children_ids += list(
313+
PeopleGroup.objects.filter(
314+
organization=people_group.organization,
315+
parent__isnull=True,
316+
is_root=False,
317+
).values_list("id", flat=True)
318+
)
319+
children = [mapping.get(child) for child in children_ids if child in mapping]
320+
return PeopleGroupHierarchySerializer(children, many=True, context=context).data
321+
322+
267323
class PeopleGroupAddTeamMembersSerializer(serializers.Serializer):
268324
people_group = HiddenPrimaryKeyRelatedField(
269325
required=False, write_only=True, queryset=PeopleGroup.objects.all()
@@ -386,23 +442,23 @@ def get_hierarchy(self, obj: PeopleGroup) -> List[Dict[str, Union[str, int]]]:
386442
while obj.parent and not obj.parent.is_root:
387443
obj = obj.parent
388444
if obj in queryset:
389-
hierarchy.append({"id": obj.id, "slug": obj.slug, "name": obj.name})
445+
hierarchy.append(
446+
PeopleGroupSuperLightSerializer(obj, context=self.context).data
447+
)
390448
return [{"order": i, **h} for i, h in enumerate(hierarchy[::-1])]
391449

392450
def get_children(self, obj: PeopleGroup) -> List[Dict[str, Union[str, int]]]:
393451
request = self.context.get("request")
394452
queryset = (
395-
request.user.get_people_group_queryset() & obj.children.all().distinct()
453+
request.user.get_people_group_queryset()
454+
.select_related("organization")
455+
.filter(parent=obj)
456+
.order_by("name")
457+
.distinct()
396458
)
397-
return [
398-
{
399-
"id": child.id,
400-
"slug": child.slug,
401-
"name": child.name,
402-
"organization": child.organization.code,
403-
}
404-
for child in queryset.order_by("name")
405-
]
459+
return PeopleGroupSuperLightSerializer(
460+
queryset, many=True, context=self.context
461+
).data
406462

407463
def validate_featured_projects(self, projects: List[Project]) -> List[Project]:
408464
request = self.context.get("request")

apps/accounts/views.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
EmptyPayloadResponseSerializer,
6666
PeopleGroupAddFeaturedProjectsSerializer,
6767
PeopleGroupAddTeamMembersSerializer,
68+
PeopleGroupHierarchySerializer,
6869
PeopleGroupLightSerializer,
6970
PeopleGroupRemoveFeaturedProjectsSerializer,
7071
PeopleGroupRemoveTeamMembersSerializer,
@@ -830,7 +831,10 @@ def project(self, request, *args, **kwargs):
830831
def hierarchy(self, request, *args, **kwargs):
831832
people_group = self.get_object()
832833
return Response(
833-
people_group.get_hierarchy(self.request.user), status=status.HTTP_200_OK
834+
PeopleGroupHierarchySerializer(
835+
people_group, context={"request": request}
836+
).data,
837+
status=status.HTTP_200_OK,
834838
)
835839

836840

apps/organizations/models.py

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from django.contrib.auth.models import Group, Permission
44
from django.contrib.contenttypes.models import ContentType
5-
from django.contrib.postgres.aggregates import ArrayAgg
65
from django.contrib.postgres.fields import ArrayField
76
from django.db import models
87
from django.db.models import Q, QuerySet, UniqueConstraint
@@ -557,36 +556,6 @@ def update_or_create_root(cls, organization: "Organization"):
557556
)
558557
return root_group
559558

560-
@classmethod
561-
def _get_hierarchy(cls, categories: dict[int, dict], category_id: int):
562-
from apps.files.serializers import ImageSerializer
563-
564-
return {
565-
"id": categories[category_id].id,
566-
"slug": categories[category_id].slug,
567-
"name": categories[category_id].name,
568-
"background_color": categories[category_id].background_color,
569-
"foreground_color": categories[category_id].foreground_color,
570-
"background_image": (
571-
ImageSerializer(categories[category_id].background_image).data
572-
if categories[category_id].background_image
573-
else None
574-
),
575-
"children": [
576-
cls._get_hierarchy(categories, child)
577-
for child in categories[category_id].children_ids
578-
if child is not None
579-
],
580-
}
581-
582-
def get_hierarchy(self):
583-
# This would be better with a recursive serializer, but it doubles the query time
584-
categories = ProjectCategory.objects.filter(
585-
organization=self.organization.pk
586-
).annotate(children_ids=ArrayAgg("children"))
587-
categories = {category.id: category for category in categories}
588-
return self._get_hierarchy(categories, self.id)
589-
590559

591560
class CategoryFollow(HasOwner, OrganizationRelated, models.Model):
592561
"""Represent a user following a project category.

apps/organizations/serializers.py

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,20 @@ def get_related_organizations(self) -> List[Organization]:
427427
return [self.instance.organization] if self.instance else []
428428

429429

430+
class ProjectCategorySuperLightSerializer(
431+
AutoTranslatedModelSerializer,
432+
serializers.ModelSerializer,
433+
):
434+
435+
class Meta:
436+
model = ProjectCategory
437+
fields = [
438+
"id",
439+
"slug",
440+
"name",
441+
]
442+
443+
430444
class ProjectCategoryLightSerializer(
431445
AutoTranslatedModelSerializer,
432446
OrganizationRelatedSerializer,
@@ -551,6 +565,53 @@ def get_string_images_kwargs(
551565
}
552566

553567

568+
class ProjectCategoryHierarchySerializer(
569+
AutoTranslatedModelSerializer,
570+
OrganizationRelatedSerializer,
571+
serializers.ModelSerializer,
572+
):
573+
children = serializers.SerializerMethodField()
574+
background_image = ImageSerializer(read_only=True)
575+
576+
class Meta:
577+
model = ProjectCategory
578+
read_only_fields = [
579+
"id",
580+
"slug",
581+
"name",
582+
"background_color",
583+
"foreground_color",
584+
"background_image",
585+
"children",
586+
]
587+
fields = read_only_fields
588+
589+
def get_children(
590+
self, category: ProjectCategory
591+
) -> List[Dict[str, Union[str, int]]]:
592+
context = self.context
593+
mapping = context.get("mapping")
594+
if not mapping:
595+
queryset = ProjectCategory.objects.filter(
596+
organization=category.organization
597+
)
598+
mapping = {cat.id: cat for cat in queryset}
599+
context["mapping"] = mapping
600+
children_ids = list(category.children.all().values_list("id", flat=True))
601+
if category.is_root:
602+
children_ids += list(
603+
ProjectCategory.objects.filter(
604+
organization=category.organization,
605+
parent__isnull=True,
606+
is_root=False,
607+
).values_list("id", flat=True)
608+
)
609+
children = [mapping.get(child) for child in children_ids if child in mapping]
610+
return ProjectCategoryHierarchySerializer(
611+
children, many=True, context=context
612+
).data
613+
614+
554615
class ProjectCategorySerializer(
555616
StringsImagesSerializer,
556617
AutoTranslatedModelSerializer,
@@ -614,18 +675,16 @@ def get_hierarchy(self, obj: ProjectCategory) -> List[Dict[str, Union[str, int]]
614675
hierarchy = []
615676
while obj.parent and not obj.parent.is_root:
616677
obj = obj.parent
617-
hierarchy.append({"id": obj.id, "slug": obj.slug, "name": obj.name})
678+
hierarchy.append(
679+
ProjectCategorySuperLightSerializer(obj, context=self.context).data
680+
)
618681
return [{"order": i, **h} for i, h in enumerate(hierarchy[::-1])]
619682

620683
def get_children(self, obj: ProjectCategory) -> List[Dict[str, Union[str, int]]]:
621-
return [
622-
{
623-
"id": child.id,
624-
"slug": child.slug,
625-
"name": child.name,
626-
}
627-
for child in obj.children.all().order_by("name")
628-
]
684+
queryset = obj.children.all().order_by("name")
685+
return ProjectCategorySuperLightSerializer(
686+
queryset, many=True, context=self.context
687+
).data
629688

630689
def get_projects_count(self, obj: ProjectCategory) -> int:
631690
return obj.projects.count()

0 commit comments

Comments
 (0)