Coverage for app/venv/lib/python3.14/site-packages/weblate/trans/models/__init__.py: 53%

112 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 07:15 +0000

1# Copyright © Michal Čihař <michal@weblate.org> 

2# 

3# SPDX-License-Identifier: GPL-3.0-or-later 

4 

5import os 

6from functools import partial 

7 

8from django.db import transaction 

9from django.db.models.signals import m2m_changed, post_delete, post_save, pre_delete 

10from django.dispatch import receiver 

11 

12from weblate.trans.models._conf import WeblateConf 

13from weblate.trans.models.agreement import ContributorAgreement 

14from weblate.trans.models.alert import Alert 

15from weblate.trans.models.announcement import Announcement 

16from weblate.trans.models.category import Category 

17from weblate.trans.models.change import Change 

18from weblate.trans.models.comment import Comment 

19from weblate.trans.models.component import Component 

20from weblate.trans.models.componentlist import AutoComponentList, ComponentList 

21from weblate.trans.models.label import Label 

22from weblate.trans.models.pending import PendingUnitChange 

23from weblate.trans.models.project import CommitPolicyChoices, Project 

24from weblate.trans.models.suggestion import Suggestion, Vote 

25from weblate.trans.models.translation import Translation 

26from weblate.trans.models.unit import Unit 

27from weblate.trans.models.variant import Variant 

28from weblate.trans.models.workflow import WorkflowSetting 

29from weblate.trans.signals import user_pre_delete 

30from weblate.utils.decorators import disable_for_loaddata 

31from weblate.utils.files import remove_tree 

32 

33__all__ = [ 

34 "Alert", 

35 "Announcement", 

36 "Category", 

37 "Change", 

38 "Comment", 

39 "CommitPolicyChoices", 

40 "Component", 

41 "ComponentList", 

42 "ContributorAgreement", 

43 "Label", 

44 "PendingUnitChange", 

45 "Project", 

46 "Suggestion", 

47 "Translation", 

48 "Unit", 

49 "Variant", 

50 "Vote", 

51 "WeblateConf", 

52 "WorkflowSetting", 

53] 

54 

55 

56def delete_object_dir(instance) -> None: 

57 """Remove path if it exists.""" 

58 project_path = instance.full_path 

59 if os.path.exists(project_path): 

60 remove_tree(project_path) 

61 

62 

63@receiver(post_delete, sender=Project) 

64def project_post_delete(sender, instance, **kwargs) -> None: 

65 """ 

66 Project deletion hook. 

67 

68 - delete project directory 

69 - update stats 

70 """ 

71 # Update stats 

72 transaction.on_commit(instance.stats.update_parents) 

73 instance.stats.delete() 

74 

75 # Remove directory 

76 delete_object_dir(instance) 

77 

78 

79@receiver(pre_delete, sender=Component) 

80def component_pre_delete(sender, instance, **kwargs) -> None: 

81 # Collect list of stats to update, this can't be done after removal 

82 instance.stats.collect_update_objects() 

83 

84 

85@receiver(post_delete, sender=Component) 

86def component_post_delete(sender, instance, **kwargs) -> None: 

87 """ 

88 Component deletion hook. 

89 

90 - delete component directory 

91 - update stats, this is accompanied by component_pre_delete 

92 """ 

93 # Update stats 

94 transaction.on_commit(instance.stats.update_parents) 

95 instance.stats.delete() 

96 

97 # Do not delete linked components 

98 if not instance.is_repo_link: 

99 delete_object_dir(instance) 

100 

101 

102@receiver(post_delete, sender=Translation) 

103def translation_post_delete(sender, instance, **kwargs) -> None: 

104 """Delete translation stats on translation deletion.""" 

105 transaction.on_commit(instance.stats.delete) 

106 

107 

108@receiver(m2m_changed, sender=Component.links.through) 

109@disable_for_loaddata 

110def update_project_stats_link(sender, instance, action, pk_set, **kwargs) -> None: 

111 from weblate.utils.tasks import update_project_stats_link 

112 

113 if action in {"post_add", "post_remove", "post_clear"}: 

114 for pk in pk_set: 

115 update_project_stats_link.delay_on_commit(pk) 

116 

117 

118@receiver(m2m_changed, sender=Unit.labels.through) 

119@disable_for_loaddata 

120def change_labels(sender, instance, action, pk_set, **kwargs) -> None: 

121 """Update unit labels.""" 

122 if ( 

123 action not in {"post_add", "post_remove", "post_clear"} 

124 or (action != "post_clear" and not pk_set) 

125 or not instance.is_source 

126 ): 

127 return 

128 if not instance.is_batch_update: 

129 instance.translation.component.invalidate_cache() 

130 

131 

132@receiver(pre_delete, sender=Label) 

133def label_pre_delete(sender, instance, **kwargs) -> None: 

134 instance.project.collect_label_cleanup(instance) 

135 

136 

137@receiver(post_delete, sender=Label) 

138def label_post_delete(sender, instance, **kwargs) -> None: 

139 """Invalidate label stats on its deletion.""" 

140 transaction.on_commit( 

141 partial(instance.project.cleanup_label_stats, name=instance.name) 

142 ) 

143 

144 

145@receiver(user_pre_delete) 

146def user_commit_pending(sender, instance, **kwargs) -> None: 

147 """Commit pending changes for user on account removal.""" 

148 # All user changes 

149 all_changes = Change.objects.last_changes(instance).filter(user=instance) 

150 

151 # Filter where project is active 

152 user_translation_ids = all_changes.values_list("translation", flat=True).distinct() 

153 

154 # Commit changes where user is last author 

155 for translation in Translation.objects.filter(pk__in=user_translation_ids): 

156 try: 

157 last_author = translation.change_set.content()[0].author 

158 except IndexError: 

159 # Non content changes 

160 continue 

161 if last_author == instance: 

162 translation.commit_pending("user delete", None) 

163 

164 

165@receiver(m2m_changed, sender=ComponentList.components.through) 

166@disable_for_loaddata 

167def change_componentlist(sender, instance, action, **kwargs) -> None: 

168 if not action.startswith("post_"): 

169 return 

170 transaction.on_commit(instance.stats.update_stats) 

171 

172 

173@receiver(post_save, sender=AutoComponentList) 

174@disable_for_loaddata 

175def auto_componentlist(sender, instance, **kwargs) -> None: 

176 for component in Component.objects.iterator(): 

177 instance.check_match(component) 

178 

179 

180@receiver(post_save, sender=Project) 

181@disable_for_loaddata 

182def auto_project_componentlist(sender, instance, **kwargs) -> None: 

183 for component in instance.component_set.iterator(): 

184 auto_component_list(sender, component) 

185 

186 

187@receiver(post_save, sender=Component) 

188@disable_for_loaddata 

189def auto_component_list(sender, instance, **kwargs) -> None: 

190 for auto in AutoComponentList.objects.iterator(): 190 ↛ 191line 190 didn't jump to line 191 because the loop on line 190 never started

191 auto.check_match(instance) 

192 

193 

194@receiver(post_delete, sender=Component) 

195@disable_for_loaddata 

196def post_delete_linked(sender, instance, **kwargs) -> None: 

197 # When removing project, the linked component might be already deleted now 

198 try: 

199 if instance.linked_component: 

200 instance.linked_component.update_alerts() 

201 except Component.DoesNotExist: 

202 pass 

203 

204 

205@receiver(post_save, sender=Comment) 

206@receiver(post_save, sender=Suggestion) 

207@disable_for_loaddata 

208def stats_invalidate(sender, instance, **kwargs) -> None: 

209 """Invalidate stats on new comment or suggestion.""" 

210 instance.unit.invalidate_related_cache()