Coverage for backend/django/core/auxiliary/models/ProjectFolder.py: 90%

78 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-07-22 05:22 +0000

1from django.core.exceptions import ValidationError 

2from django.db import models, router 

3from django.db.models.functions import Lower 

4 

5from authentication.user.models import User 

6 

7 

8PROJECT_FOLDER_STRUCTURE_FIELDS = frozenset( 

9 {"owner", "owner_id", "parent", "parent_id"} 

10) 

11 

12 

13class ProjectFolderQuerySet(models.QuerySet): 

14 """Keep structural folder writes behind the model's validation boundary.""" 

15 

16 @staticmethod 

17 def _reject_structural_fields(fields) -> None: 

18 """Reject bulk operations that bypass immutable owner/parent checks.""" 

19 

20 structural_fields = PROJECT_FOLDER_STRUCTURE_FIELDS.intersection(fields) 

21 if structural_fields: 

22 field_names = ", ".join(sorted(structural_fields)) 

23 raise ValidationError( 

24 f"Project folder owner and parent are immutable ({field_names})." 

25 ) 

26 

27 def update(self, **kwargs): 

28 """Allow lifecycle updates while blocking direct hierarchy rewrites.""" 

29 

30 self._reject_structural_fields(kwargs) 

31 return super().update(**kwargs) 

32 

33 def bulk_update(self, objs, fields, batch_size=None): 

34 """Allow bulk lifecycle updates but never bulk structural changes.""" 

35 

36 self._reject_structural_fields(fields) 

37 return super().bulk_update(objs, fields, batch_size=batch_size) 

38 

39 def bulk_create( 

40 self, 

41 objs, 

42 batch_size=None, 

43 ignore_conflicts=False, 

44 update_conflicts=False, 

45 update_fields=None, 

46 unique_fields=None, 

47 ): 

48 """Validate every direct bulk insert against the parent-owner invariant.""" 

49 

50 self._reject_structural_fields(update_fields or ()) 

51 folders = list(objs) 

52 self.model.validate_bulk_structures(folders, using=self.db) 

53 return super().bulk_create( 

54 folders, 

55 batch_size=batch_size, 

56 ignore_conflicts=ignore_conflicts, 

57 update_conflicts=update_conflicts, 

58 update_fields=update_fields, 

59 unique_fields=unique_fields, 

60 ) 

61 

62 

63class ProjectFolderManager(models.Manager.from_queryset(ProjectFolderQuerySet)): 

64 """Manager exposing the guarded ProjectFolder queryset.""" 

65 

66 

67class ProjectFolder(models.Model): 

68 """A stable, owner-scoped folder in the project hierarchy.""" 

69 

70 owner = models.ForeignKey( 

71 User, 

72 on_delete=models.CASCADE, 

73 related_name="project_folders", 

74 ) 

75 parent = models.ForeignKey( 

76 "self", 

77 null=True, 

78 blank=True, 

79 on_delete=models.CASCADE, 

80 related_name="children", 

81 ) 

82 name = models.CharField(max_length=64) 

83 is_binned = models.BooleanField(default=False) 

84 binned_at = models.DateTimeField(null=True, blank=True) 

85 created_at = models.DateTimeField(auto_now_add=True) 

86 updated_at = models.DateTimeField(auto_now=True) 

87 

88 objects = ProjectFolderManager() 

89 

90 class Meta: 

91 constraints = [ 

92 models.UniqueConstraint( 

93 Lower("name"), 

94 "owner", 

95 condition=models.Q(parent__isnull=True), 

96 name="unique_root_project_folder_name_per_owner", 

97 ), 

98 models.UniqueConstraint( 

99 Lower("name"), 

100 "owner", 

101 "parent", 

102 condition=models.Q(parent__isnull=False), 

103 name="unique_child_project_folder_name_per_owner", 

104 ), 

105 models.CheckConstraint( 

106 check=~models.Q(parent=models.F("id")), 

107 name="project_folder_cannot_parent_itself", 

108 ), 

109 ] 

110 indexes = [ 

111 models.Index( 

112 fields=["owner", "is_binned", "parent"], 

113 name="folder_owner_bin_parent_idx", 

114 ) 

115 ] 

116 

117 @classmethod 

118 def validate_bulk_structures(cls, folders, *, using: str) -> None: 

119 """Validate parent ownership for inserts that bypass ``save()``.""" 

120 

121 parent_ids = { 

122 folder.parent_id for folder in folders if folder.parent_id is not None 

123 } 

124 parent_owners = dict( 

125 cls.objects.using(using) 

126 .filter(pk__in=parent_ids) 

127 .values_list("pk", "owner_id") 

128 ) 

129 for folder in folders: 

130 if folder.owner_id is None: 130 ↛ 131line 130 didn't jump to line 131 because the condition on line 130 was never true

131 raise ValidationError({"owner": "A project folder requires an owner."}) 

132 if folder.parent_id is None: 

133 continue 

134 parent_owner_id = parent_owners.get(folder.parent_id) 

135 if parent_owner_id is None: 135 ↛ 136line 135 didn't jump to line 136 because the condition on line 135 was never true

136 raise ValidationError({"parent": "Project folder parent not found."}) 

137 if parent_owner_id != folder.owner_id: 

138 raise ValidationError( 

139 { 

140 "parent": ( 

141 "A project folder must have the same owner as its parent." 

142 ) 

143 } 

144 ) 

145 

146 def _validate_structure(self, *, using: str) -> None: 

147 """Enforce immutable ownership and same-owner parentage before a write.""" 

148 

149 if self.owner_id is None: 149 ↛ 150line 149 didn't jump to line 150 because the condition on line 149 was never true

150 raise ValidationError({"owner": "A project folder requires an owner."}) 

151 

152 queryset = type(self).objects.using(using) 

153 # ``Model(pk=existing_id, ...).save(force_update=True)`` still has 

154 # ``_state.adding=True``. Look up any existing primary key regardless 

155 # of that in-memory flag so detached instances cannot rewrite the 

156 # immutable hierarchy. 

157 if self.pk is not None: 

158 persisted = ( 

159 queryset.filter(pk=self.pk) 

160 .values("owner_id", "parent_id") 

161 .first() 

162 ) 

163 if persisted is not None and ( 163 ↛ 171line 163 didn't jump to line 171 because the condition on line 163 was always true

164 persisted["owner_id"] != self.owner_id 

165 or persisted["parent_id"] != self.parent_id 

166 ): 

167 raise ValidationError( 

168 "Project folder owner and parent cannot be changed after creation." 

169 ) 

170 

171 if self.parent_id is None: 

172 return 

173 parent_owner_id = ( 

174 queryset.filter(pk=self.parent_id) 

175 .values_list("owner_id", flat=True) 

176 .first() 

177 ) 

178 if parent_owner_id is None: 178 ↛ 179line 178 didn't jump to line 179 because the condition on line 178 was never true

179 raise ValidationError({"parent": "Project folder parent not found."}) 

180 if parent_owner_id != self.owner_id: 

181 raise ValidationError( 

182 {"parent": "A project folder must have the same owner as its parent."} 

183 ) 

184 

185 def clean(self): 

186 """Validate structural invariants during explicit model validation.""" 

187 

188 super().clean() 

189 using = self._state.db or router.db_for_write(type(self), instance=self) 

190 self._validate_structure(using=using) 

191 

192 def save(self, *args, **kwargs): 

193 """Make direct ORM saves obey the folder structural boundary.""" 

194 

195 update_fields = kwargs.get("update_fields") 

196 validates_structure = ( 

197 self._state.adding 

198 or update_fields is None 

199 or bool(PROJECT_FOLDER_STRUCTURE_FIELDS.intersection(update_fields)) 

200 ) 

201 if validates_structure: 

202 using = ( 

203 kwargs.get("using") 

204 or self._state.db 

205 or router.db_for_write(type(self), instance=self) 

206 ) 

207 self._validate_structure(using=using) 

208 return super().save(*args, **kwargs) 

209 

210 def __str__(self) -> str: 

211 return self.name