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

149 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 

4from __future__ import annotations 

5 

6from typing import TYPE_CHECKING 

7 

8from django.db.models import Count, Q 

9from django.http import Http404 

10from django.shortcuts import get_object_or_404, redirect 

11from django.urls import reverse 

12from django.utils.translation import gettext 

13from django.views.generic import ListView 

14 

15from weblate.auth.models import AuthenticatedHttpRequest 

16from weblate.checks.models import CHECKS, Check 

17from weblate.lang.models import Language 

18from weblate.trans.models import Component, Project, Translation, Unit 

19from weblate.utils.random import get_random_identifier 

20from weblate.utils.state import STATE_TRANSLATED 

21from weblate.utils.stats import ProjectLanguage 

22from weblate.utils.views import PathViewMixin 

23 

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

25 from weblate.auth.models import AuthenticatedHttpRequest 

26 

27 

28class CheckWrapper: 

29 check_obj: Check | None 

30 

31 def __init__( 

32 self, 

33 name: str, 

34 check_count: int, 

35 dismissed_check_count: int, 

36 active_check_count: int, 

37 translated_check_count: int, 

38 path_object, 

39 ) -> None: 

40 self.pk = get_random_identifier() 

41 self.name = name 

42 try: 

43 self.check_obj = CHECKS[name] 

44 except KeyError: 

45 self.check_obj = None 

46 self.check_count = check_count 

47 self.dismissed_check_count = dismissed_check_count 

48 self.active_check_count = active_check_count 

49 self.translated_check_count = translated_check_count 

50 self.row_title = self.check_obj.name if self.check_obj else self.name 

51 kwargs = {"name": self.name} 

52 if path_object is not None: 

53 kwargs["path"] = path_object.get_url_path() 

54 self.row_url = reverse("checks", kwargs=kwargs) 

55 

56 

57class CheckList(PathViewMixin, ListView): 

58 supported_path_types = ( 

59 None, 

60 Project, 

61 Component, 

62 Translation, 

63 Language, 

64 ProjectLanguage, 

65 ) 

66 template_name = "check_list.html" 

67 request: AuthenticatedHttpRequest 

68 

69 def annotate(self, queryset, prefix: str): 

70 id_field = f"{prefix}unit__check" 

71 return queryset.annotate( 

72 check_count=Count(id_field), 

73 dismissed_check_count=Count( 

74 id_field, filter=Q(**{f"{prefix}unit__check__dismissed": True}) 

75 ), 

76 active_check_count=Count( 

77 id_field, filter=Q(**{f"{prefix}unit__check__dismissed": False}) 

78 ), 

79 translated_check_count=Count( 

80 id_field, 

81 filter=Q( 

82 **{ 

83 f"{prefix}unit__check__dismissed": False, 

84 f"{prefix}unit__state__gte": STATE_TRANSLATED, 

85 }, 

86 ), 

87 ), 

88 ) 

89 

90 def postprocess_queryset(self, result): 

91 # Annotate with progress indicator and attributes 

92 max_value = max((item.check_count for item in result), default=0) 

93 for item in result: 

94 item.check_progress = item.check_count * 100 // max_value 

95 if not hasattr(item, "check_obj"): 

96 item.check_obj = self.check_obj 

97 if not hasattr(item, "row_title"): 

98 if isinstance(item, Translation): 

99 if isinstance(self.path_object, ProjectLanguage): 

100 item.row_title = item.component.name 

101 else: 

102 item.row_title = item.language.name 

103 elif isinstance(item, ProjectLanguage): 

104 item.row_title = item.project.name 

105 else: 

106 item.row_title = item.name 

107 if not hasattr(item, "row_url"): 

108 item.row_url = reverse( 

109 "checks", 

110 kwargs={ 

111 "name": self.check_obj.check_id if self.check_obj else "-", 

112 "path": item.get_url_path(), 

113 }, 

114 ) 

115 return result 

116 

117 def get_queryset(self): 

118 if self.check_obj is None: 

119 if self.path_object is None: 

120 all_checks = Check.objects.filter_access(self.request.user) 

121 elif isinstance(self.path_object, Project): 

122 all_checks = Check.objects.filter( 

123 unit__translation__component__project=self.path_object 

124 ) 

125 elif isinstance(self.path_object, Component): 

126 all_checks = Check.objects.filter( 

127 unit__translation__component=self.path_object 

128 ) 

129 elif isinstance(self.path_object, Translation): 

130 all_checks = Check.objects.filter(unit__translation=self.path_object) 

131 elif isinstance(self.path_object, Language): 

132 all_checks = Check.objects.filter( 

133 unit__translation__language=self.path_object 

134 ) 

135 elif isinstance(self.path_object, ProjectLanguage): 

136 all_checks = Check.objects.filter( 

137 unit__translation__language=self.path_object.language, 

138 unit__translation__component__project=self.path_object.project, 

139 ) 

140 else: 

141 msg = f"Unsupported {self.path_object}" 

142 raise TypeError(msg) 

143 result = [ 

144 CheckWrapper(**item, path_object=self.path_object) 

145 for item in all_checks.values("name").annotate( 

146 check_count=Count("id"), 

147 dismissed_check_count=Count("id", filter=Q(dismissed=True)), 

148 active_check_count=Count("id", filter=Q(dismissed=False)), 

149 translated_check_count=Count( 

150 "id", 

151 filter=Q(dismissed=False, unit__state__gte=STATE_TRANSLATED), 

152 ), 

153 ) 

154 ] 

155 elif self.path_object is None: 

156 result = self.annotate( 

157 self.request.user.allowed_projects.filter( 

158 component__translation__unit__check__name=self.check_obj.check_id 

159 ), 

160 "component__translation__", 

161 ).order() 

162 elif isinstance(self.path_object, Project): 

163 result = self.annotate( 

164 Component.objects.filter_access(self.request.user) 

165 .filter( 

166 translation__unit__check__name=self.check_obj.check_id, 

167 project=self.path_object, 

168 ) 

169 .prefetch_related("project"), 

170 "translation__", 

171 ).order() 

172 elif isinstance(self.path_object, Component): 

173 result = self.annotate( 

174 Translation.objects.filter( 

175 component=self.path_object, 

176 unit__check__name=self.check_obj.check_id, 

177 ) 

178 .select_related("language") 

179 .prefetch_related("component__project"), 

180 "", 

181 ).order_by("language__code") 

182 elif isinstance(self.path_object, Language): 

183 result = [ 

184 ProjectLanguage(project=obj, language=self.path_object) 

185 for obj in self.annotate( 

186 self.request.user.allowed_projects.filter( 

187 component__translation__language=self.path_object, 

188 component__translation__unit__check__name=self.check_obj.check_id, 

189 ), 

190 "component__translation__", 

191 ).order() 

192 ] 

193 # Mirror counts 

194 for item in result: 

195 item.check_count = item.project.check_count 

196 item.dismissed_check_count = item.project.dismissed_check_count 

197 item.active_check_count = item.project.active_check_count 

198 item.translated_check_count = item.project.translated_check_count 

199 elif isinstance(self.path_object, ProjectLanguage): 

200 result = self.annotate( 

201 Translation.objects.filter( 

202 component__project=self.path_object.project, 

203 language=self.path_object.language, 

204 unit__check__name=self.check_obj.check_id, 

205 ) 

206 .select_related("language") 

207 .prefetch_related("component__project"), 

208 "", 

209 ).order_by("language__code") 

210 else: 

211 # Translation should never reach this, it is handled in get() 

212 msg = f"Unsupported {self.path_object}" 

213 raise TypeError(msg) 

214 

215 return self.postprocess_queryset(result) 

216 

217 def get_context_data(self, **kwargs): 

218 context = super().get_context_data(**kwargs) 

219 context["check"] = self.check_obj 

220 context["path_object"] = self.path_object 

221 context["bootstrap_5"] = True 

222 if self.check_obj is None: 

223 if self.path_object is None: 

224 context["title"] = gettext("Failing checks") 

225 else: 

226 context["title"] = f"{gettext('Failing checks')} / {self.path_object}" 

227 elif self.path_object is None: 

228 context["title"] = self.check_obj.name 

229 else: 

230 context["title"] = f"{self.check_obj.name} / {self.path_object}" 

231 if self.check_obj is None: 

232 context["column_title"] = gettext("Check") 

233 elif self.path_object is None: 

234 context["column_title"] = gettext("Project") 

235 elif isinstance(self.path_object, Project): 

236 context["column_title"] = gettext("Component") 

237 elif isinstance(self.path_object, Component): 

238 context["column_title"] = gettext("Translation") 

239 context["translate_links"] = True 

240 elif isinstance(self.path_object, Language): 

241 context["column_title"] = gettext("Project") 

242 elif isinstance(self.path_object, ProjectLanguage): 

243 context["column_title"] = gettext("Component") 

244 context["translate_links"] = True 

245 else: 

246 msg = f"Type not supported: {self.path_object}" 

247 raise TypeError(msg) 

248 return context 

249 

250 def setup(self, request: AuthenticatedHttpRequest, **kwargs) -> None: 

251 super().setup(request, **kwargs) 

252 self.check_obj = None 

253 name = kwargs.get("name") 

254 if name and name != "-": 

255 try: 

256 self.check_obj = CHECKS[name] 

257 except KeyError as error: 

258 msg = "No check matches the given query." 

259 raise Http404(msg) from error 

260 

261 def get(self, request: AuthenticatedHttpRequest, *args, **kwargs): 

262 if isinstance(self.path_object, Translation) and self.check_obj: 

263 return redirect( 

264 f"{self.path_object.get_translate_url()}?q={self.check_obj.url_id} OR dismissed_{self.check_obj.url_id}" 

265 ) 

266 return super().get(request, *args, **kwargs) 

267 

268 

269def render_check(request: AuthenticatedHttpRequest, unit_id, check_id): 

270 """Render endpoint for checks.""" 

271 try: 

272 obj = Check.objects.get(unit_id=unit_id, name=check_id) 

273 except Check.DoesNotExist: 

274 unit = get_object_or_404(Unit, pk=int(unit_id)) 

275 obj = Check(unit=unit, dismissed=False, name=check_id) 

276 request.user.check_access_component(obj.unit.translation.component) 

277 

278 if obj.check_obj is None: 

279 msg = "No check object found." 

280 raise Http404(msg) 

281 

282 return obj.check_obj.render(request, obj.unit)