Coverage for backend/django/core/auxiliary/serializers/ProjectFolderSerializer.py: 91%
79 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
1from rest_framework import serializers
3from core.auxiliary.models.ProjectFolder import ProjectFolder
4from core.auxiliary.services.project_folders import (
5 ProjectFolderConflictError,
6 ProjectFolderNotFoundError,
7 create_project_folder,
8 normalize_project_folder_name,
9 rename_project_folder,
10)
13class ProjectFolderSerializer(serializers.ModelSerializer):
14 """Serialize owner-scoped folder rows and create them transactionally."""
16 name = serializers.CharField(trim_whitespace=False)
17 parent = serializers.PrimaryKeyRelatedField(
18 queryset=ProjectFolder.objects.all(),
19 allow_null=True,
20 )
21 project_count = serializers.SerializerMethodField()
23 class Meta:
24 model = ProjectFolder
25 fields = ["id", "name", "parent", "project_count"]
26 read_only_fields = ["id", "project_count"]
28 def __init__(self, *args, **kwargs):
29 super().__init__(*args, **kwargs)
30 request = self.context.get("request")
31 user = getattr(request, "user", None)
32 if getattr(user, "is_authenticated", False):
33 self.fields["parent"].queryset = ProjectFolder.objects.filter(
34 owner=user,
35 is_binned=False,
36 )
38 def get_project_count(self, folder: ProjectFolder) -> int:
39 """Default newly created or reactivated empty folders to zero projects."""
41 return getattr(folder, "project_count", 0)
43 def validate_name(self, value: str) -> str:
44 """Normalize a display label without assigning path semantics to it."""
46 try:
47 return normalize_project_folder_name(value)
48 except ValueError as exc:
49 raise serializers.ValidationError(str(exc)) from exc
51 def validate_parent(self, parent: ProjectFolder | None) -> ProjectFolder | None:
52 """Restrict new children to active folders owned by the request user."""
54 if parent is None:
55 return None
56 request = self.context.get("request")
57 user_id = getattr(getattr(request, "user", None), "id", None)
58 if parent.owner_id != user_id or parent.is_binned: 58 ↛ 59line 58 didn't jump to line 59 because the condition on line 58 was never true
59 raise serializers.ValidationError("Parent project folder not found.")
60 return parent
62 def create(self, validated_data) -> ProjectFolder:
63 """Delegate the lock/revalidation and binned-row reuse workflow."""
65 request = self.context["request"]
66 parent = validated_data.get("parent")
67 try:
68 return create_project_folder(
69 user=request.user,
70 name=validated_data["name"],
71 parent_id=parent.pk if parent is not None else None,
72 )
73 except ProjectFolderNotFoundError as exc:
74 raise serializers.ValidationError({"parent": str(exc)}) from exc
75 except ProjectFolderConflictError as exc:
76 raise serializers.ValidationError({"name": str(exc)}) from exc
78 def update(self, instance, validated_data) -> ProjectFolder:
79 """Rename through the owner-scoped transactional workflow."""
81 if ( 81 ↛ 85line 81 didn't jump to line 85 because the condition on line 81 was never true
82 "parent" in validated_data
83 and validated_data["parent"] != instance.parent
84 ):
85 raise serializers.ValidationError(
86 {"parent": "Project folder parent cannot be changed."}
87 )
88 try:
89 return rename_project_folder(
90 user=self.context["request"].user,
91 folder_id=instance.pk,
92 name=validated_data.get("name", instance.name),
93 )
94 except ProjectFolderNotFoundError as exc:
95 raise serializers.ValidationError({"detail": str(exc)}) from exc
96 except ProjectFolderConflictError as exc:
97 raise serializers.ValidationError({"name": str(exc)}) from exc
100class ProjectFolderPageSerializer(serializers.Serializer):
101 """Describe one bounded page of folders."""
103 next = serializers.URLField(allow_null=True)
104 previous = serializers.URLField(allow_null=True)
105 pages = serializers.IntegerField()
106 count = serializers.IntegerField()
107 page_size = serializers.IntegerField()
108 results = ProjectFolderSerializer(many=True)
111class ProjectFolderNavigationSerializer(serializers.ModelSerializer):
112 """Serialize the minimum folder fields required for path navigation."""
114 class Meta:
115 model = ProjectFolder
116 fields = ["id", "name", "parent"]
119class ProjectFolderWorkspaceSerializer(serializers.Serializer):
120 """Return only the navigation context and child page for one folder."""
122 current_folder = ProjectFolderNavigationSerializer(allow_null=True)
123 breadcrumbs = ProjectFolderNavigationSerializer(many=True)
124 folders = ProjectFolderPageSerializer()
125 requested_folder_missing = serializers.BooleanField()
128class ProjectFolderDestinationSerializer(serializers.Serializer):
129 """Return a constant-size, disambiguated move destination summary."""
131 id = serializers.IntegerField()
132 path = serializers.ListField(child=serializers.CharField())
133 has_omitted_ancestors = serializers.BooleanField()
134 result_position = serializers.IntegerField()
137class ProjectFolderDestinationPageSerializer(serializers.Serializer):
138 """Describe a server-filtered destination page and the root match."""
140 next = serializers.URLField(allow_null=True)
141 previous = serializers.URLField(allow_null=True)
142 pages = serializers.IntegerField()
143 count = serializers.IntegerField()
144 page_size = serializers.IntegerField()
145 results = ProjectFolderDestinationSerializer(many=True)
146 root_matches = serializers.BooleanField()