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
« 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
5import os
6from functools import partial
8from django.db import transaction
9from django.db.models.signals import m2m_changed, post_delete, post_save, pre_delete
10from django.dispatch import receiver
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
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]
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)
63@receiver(post_delete, sender=Project)
64def project_post_delete(sender, instance, **kwargs) -> None:
65 """
66 Project deletion hook.
68 - delete project directory
69 - update stats
70 """
71 # Update stats
72 transaction.on_commit(instance.stats.update_parents)
73 instance.stats.delete()
75 # Remove directory
76 delete_object_dir(instance)
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()
85@receiver(post_delete, sender=Component)
86def component_post_delete(sender, instance, **kwargs) -> None:
87 """
88 Component deletion hook.
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()
97 # Do not delete linked components
98 if not instance.is_repo_link:
99 delete_object_dir(instance)
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)
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
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)
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()
132@receiver(pre_delete, sender=Label)
133def label_pre_delete(sender, instance, **kwargs) -> None:
134 instance.project.collect_label_cleanup(instance)
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 )
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)
151 # Filter where project is active
152 user_translation_ids = all_changes.values_list("translation", flat=True).distinct()
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)
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)
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)
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)
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)
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
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()