Coverage for app/venv/lib/python3.14/site-packages/weblate/checks/models.py: 41%

96 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 

5from __future__ import annotations 

6 

7import json 

8from typing import TYPE_CHECKING 

9 

10from appconf import AppConf 

11from django.db import models 

12from django.db.models import Q 

13from django.utils.functional import cached_property 

14 

15from weblate.utils.classloader import ClassLoader 

16 

17from .base import BaseCheck 

18 

19if TYPE_CHECKING: 19 ↛ 20line 19 didn't jump to line 20 because the condition on line 19 was never true

20 from collections.abc import Generator, Iterable 

21 

22 from django_stubs_ext import StrOrPromise 

23 

24 from weblate.auth.models import User 

25 from weblate.trans.models import Unit 

26 

27 from .base import FixupType 

28 

29 

30class ChecksLoader(ClassLoader[BaseCheck]): 

31 def __init__(self) -> None: 

32 super().__init__("CHECK_LIST", base_class=BaseCheck) 

33 

34 @cached_property 

35 def source(self): 

36 return {k: v for k, v in self.items() if v.source} 

37 

38 @cached_property 

39 def target(self): 

40 return {k: v for k, v in self.items() if v.target} 

41 

42 @cached_property 

43 def glossary(self): 

44 return {k: v for k, v in self.items() if v.glossary} 

45 

46 

47# Initialize checks list 

48CHECKS = ChecksLoader() 

49 

50 

51class WeblateChecksConf(AppConf): 

52 # List of quality checks 

53 CHECK_LIST = ( 

54 "weblate.checks.same.SameCheck", 

55 "weblate.checks.chars.BeginNewlineCheck", 

56 "weblate.checks.chars.EndNewlineCheck", 

57 "weblate.checks.chars.BeginSpaceCheck", 

58 "weblate.checks.chars.EndSpaceCheck", 

59 "weblate.checks.chars.DoubleSpaceCheck", 

60 "weblate.checks.chars.EndStopCheck", 

61 "weblate.checks.chars.EndColonCheck", 

62 "weblate.checks.chars.EndQuestionCheck", 

63 "weblate.checks.chars.EndExclamationCheck", 

64 "weblate.checks.chars.EndInterrobangCheck", 

65 "weblate.checks.chars.EndEllipsisCheck", 

66 "weblate.checks.chars.EndSemicolonCheck", 

67 "weblate.checks.chars.MaxLengthCheck", 

68 "weblate.checks.chars.KashidaCheck", 

69 "weblate.checks.chars.PunctuationSpacingCheck", 

70 "weblate.checks.chars.KabyleCharactersCheck", 

71 "weblate.checks.format.PythonFormatCheck", 

72 "weblate.checks.format.PythonBraceFormatCheck", 

73 "weblate.checks.format.PHPFormatCheck", 

74 "weblate.checks.format.CFormatCheck", 

75 "weblate.checks.format.PerlFormatCheck", 

76 "weblate.checks.format.PerlBraceFormatCheck", 

77 "weblate.checks.format.JavaScriptFormatCheck", 

78 "weblate.checks.format.LuaFormatCheck", 

79 "weblate.checks.format.ObjectPascalFormatCheck", 

80 "weblate.checks.format.SchemeFormatCheck", 

81 "weblate.checks.format.CSharpFormatCheck", 

82 "weblate.checks.format.JavaFormatCheck", 

83 "weblate.checks.format.JavaMessageFormatCheck", 

84 "weblate.checks.format.PercentPlaceholdersCheck", 

85 "weblate.checks.format.VueFormattingCheck", 

86 "weblate.checks.format.I18NextInterpolationCheck", 

87 "weblate.checks.format.ESTemplateLiteralsCheck", 

88 "weblate.checks.format.AutomatticComponentsCheck", 

89 "weblate.checks.angularjs.AngularJSInterpolationCheck", 

90 "weblate.checks.icu.ICUMessageFormatCheck", 

91 "weblate.checks.icu.ICUSourceCheck", 

92 "weblate.checks.qt.QtFormatCheck", 

93 "weblate.checks.qt.QtPluralCheck", 

94 "weblate.checks.ruby.RubyFormatCheck", 

95 "weblate.checks.consistency.PluralsCheck", 

96 "weblate.checks.consistency.SamePluralsCheck", 

97 "weblate.checks.consistency.ConsistencyCheck", 

98 "weblate.checks.consistency.ReusedCheck", 

99 "weblate.checks.consistency.TranslatedCheck", 

100 "weblate.checks.chars.EscapedNewlineCountingCheck", 

101 "weblate.checks.chars.NewLineCountCheck", 

102 "weblate.checks.markup.BBCodeCheck", 

103 "weblate.checks.chars.ZeroWidthSpaceCheck", 

104 "weblate.checks.render.MaxSizeCheck", 

105 "weblate.checks.markup.XMLValidityCheck", 

106 "weblate.checks.markup.XMLTagsCheck", 

107 "weblate.checks.markup.MarkdownRefLinkCheck", 

108 "weblate.checks.markup.MarkdownLinkCheck", 

109 "weblate.checks.markup.MarkdownSyntaxCheck", 

110 "weblate.checks.markup.URLCheck", 

111 "weblate.checks.markup.SafeHTMLCheck", 

112 "weblate.checks.markup.RSTReferencesCheck", 

113 "weblate.checks.markup.RSTSyntaxCheck", 

114 "weblate.checks.placeholders.PlaceholderCheck", 

115 "weblate.checks.placeholders.RegexCheck", 

116 "weblate.checks.duplicate.DuplicateCheck", 

117 "weblate.checks.source.OptionalPluralCheck", 

118 "weblate.checks.source.EllipsisCheck", 

119 "weblate.checks.source.MultipleFailingCheck", 

120 "weblate.checks.source.LongUntranslatedCheck", 

121 "weblate.checks.format.MultipleUnnamedFormatsCheck", 

122 "weblate.checks.glossary.GlossaryCheck", 

123 "weblate.checks.glossary.ProhibitedInitialCharacterCheck", 

124 "weblate.checks.fluent.syntax.FluentSourceSyntaxCheck", 

125 "weblate.checks.fluent.syntax.FluentTargetSyntaxCheck", 

126 "weblate.checks.fluent.parts.FluentPartsCheck", 

127 "weblate.checks.fluent.references.FluentReferencesCheck", 

128 "weblate.checks.fluent.inner_html.FluentSourceInnerHTMLCheck", 

129 "weblate.checks.fluent.inner_html.FluentTargetInnerHTMLCheck", 

130 ) 

131 

132 class Meta: 

133 prefix = "" 

134 

135 

136class CheckQuerySet(models.QuerySet): 

137 def filter_access(self, user: User): 

138 result = self 

139 if user.needs_project_filter: 

140 result = result.filter( 

141 unit__translation__component__project__in=user.allowed_projects 

142 ) 

143 if user.needs_component_restrictions_filter: 

144 result = result.filter( 

145 Q(unit__translation__component__restricted=False) 

146 | Q(unit__translation__component_id__in=user.component_permissions) 

147 ) 

148 return result 

149 

150 

151class Check(models.Model): 

152 unit = models.ForeignKey( 

153 "trans.Unit", on_delete=models.deletion.CASCADE, db_index=False 

154 ) 

155 name = models.CharField(max_length=50, choices=CHECKS.get_choices()) 

156 dismissed = models.BooleanField(db_index=True, default=False) 

157 

158 objects = CheckQuerySet.as_manager() 

159 

160 class Meta: 

161 unique_together = [ # noqa: RUF012 

162 ("unit", "name"), 

163 ] 

164 verbose_name = "Quality check" 

165 verbose_name_plural = "Quality checks" 

166 

167 def __str__(self) -> str: 

168 return str(self.get_name()) 

169 

170 @cached_property 

171 def check_obj(self) -> BaseCheck | None: 

172 try: 

173 return CHECKS[self.name] 

174 except KeyError: 

175 return None 

176 

177 def is_enforced(self) -> bool: 

178 return self.name in self.unit.translation.component.enforced_checks 

179 

180 def get_description(self) -> StrOrPromise: 

181 if self.check_obj: 

182 return self.check_obj.get_description(self) 

183 return self.name 

184 

185 def get_fixup(self) -> Iterable[FixupType] | None: 

186 if self.check_obj: 

187 return self.check_obj.get_fixup(self.unit) 

188 return None 

189 

190 def get_fixup_json(self) -> str | None: 

191 fixup = self.get_fixup() 

192 if not fixup: 

193 return None 

194 return json.dumps(fixup) 

195 

196 def get_name(self) -> StrOrPromise: 

197 if self.check_obj: 

198 return self.check_obj.name 

199 return self.name 

200 

201 def get_doc_url(self, user: User | None = None) -> str: 

202 if self.check_obj: 

203 return self.check_obj.get_doc_url(user=user) 

204 return "" 

205 

206 def set_dismiss(self, *, state: bool = True, recurse: bool = True) -> None: 

207 """Set ignore flag.""" 

208 if self.dismissed != state: 

209 self.dismissed = state 

210 self.save(update_fields=["dismissed"]) 

211 self.unit.translation.invalidate_cache() 

212 if recurse: 

213 for child in Check.objects.filter( 

214 name=self.name, 

215 unit__in=self.unit.propagated_units, 

216 dismissed=not state, 

217 ).select_for_update(): 

218 child.set_dismiss(state=state, recurse=False) 

219 

220 

221def get_display_checks(unit: Unit) -> Generator[Check]: 

222 check_objects = {check.name: check for check in unit.all_checks} 

223 for check, check_obj in CHECKS.target.items(): 

224 if check_obj.should_display(unit): 

225 try: 

226 yield check_objects[check] 

227 except KeyError: 

228 yield Check(unit=unit, dismissed=False, name=check)