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

1from rest_framework import serializers 

2 

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) 

11 

12 

13class ProjectFolderSerializer(serializers.ModelSerializer): 

14 """Serialize owner-scoped folder rows and create them transactionally.""" 

15 

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() 

22 

23 class Meta: 

24 model = ProjectFolder 

25 fields = ["id", "name", "parent", "project_count"] 

26 read_only_fields = ["id", "project_count"] 

27 

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 ) 

37 

38 def get_project_count(self, folder: ProjectFolder) -> int: 

39 """Default newly created or reactivated empty folders to zero projects.""" 

40 

41 return getattr(folder, "project_count", 0) 

42 

43 def validate_name(self, value: str) -> str: 

44 """Normalize a display label without assigning path semantics to it.""" 

45 

46 try: 

47 return normalize_project_folder_name(value) 

48 except ValueError as exc: 

49 raise serializers.ValidationError(str(exc)) from exc 

50 

51 def validate_parent(self, parent: ProjectFolder | None) -> ProjectFolder | None: 

52 """Restrict new children to active folders owned by the request user.""" 

53 

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 

61 

62 def create(self, validated_data) -> ProjectFolder: 

63 """Delegate the lock/revalidation and binned-row reuse workflow.""" 

64 

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 

77 

78 def update(self, instance, validated_data) -> ProjectFolder: 

79 """Rename through the owner-scoped transactional workflow.""" 

80 

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 

98 

99 

100class ProjectFolderPageSerializer(serializers.Serializer): 

101 """Describe one bounded page of folders.""" 

102 

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) 

109 

110 

111class ProjectFolderNavigationSerializer(serializers.ModelSerializer): 

112 """Serialize the minimum folder fields required for path navigation.""" 

113 

114 class Meta: 

115 model = ProjectFolder 

116 fields = ["id", "name", "parent"] 

117 

118 

119class ProjectFolderWorkspaceSerializer(serializers.Serializer): 

120 """Return only the navigation context and child page for one folder.""" 

121 

122 current_folder = ProjectFolderNavigationSerializer(allow_null=True) 

123 breadcrumbs = ProjectFolderNavigationSerializer(many=True) 

124 folders = ProjectFolderPageSerializer() 

125 requested_folder_missing = serializers.BooleanField() 

126 

127 

128class ProjectFolderDestinationSerializer(serializers.Serializer): 

129 """Return a constant-size, disambiguated move destination summary.""" 

130 

131 id = serializers.IntegerField() 

132 path = serializers.ListField(child=serializers.CharField()) 

133 has_omitted_ancestors = serializers.BooleanField() 

134 result_position = serializers.IntegerField() 

135 

136 

137class ProjectFolderDestinationPageSerializer(serializers.Serializer): 

138 """Describe a server-filtered destination page and the root match.""" 

139 

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()