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

180 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, NotRequired, TypedDict 

7 

8from django.contrib.auth.decorators import login_required 

9from django.core.exceptions import ObjectDoesNotExist, PermissionDenied 

10from django.db.models import Count 

11from django.http import JsonResponse 

12from django.shortcuts import render 

13from django.urls import reverse 

14from django.utils.decorators import method_decorator 

15from django.utils.functional import cached_property 

16from django.utils.translation import gettext 

17from django.views.generic.base import TemplateView 

18 

19from weblate.lang.models import Language 

20from weblate.memory.forms import DeleteForm, UploadForm 

21from weblate.memory.models import Memory, MemoryImportError 

22from weblate.memory.tasks import import_memory 

23from weblate.metrics.models import Metric 

24from weblate.trans.models import Project 

25from weblate.utils import messages 

26from weblate.utils.views import ErrorFormView, parse_path 

27from weblate.wladmin.views import MENU 

28 

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

30 from weblate.auth.models import AuthenticatedHttpRequest, User 

31 

32CD_TEMPLATE = 'attachment; filename="weblate-memory.{}"' 

33 

34 

35class ObjectsDict(TypedDict): 

36 project: NotRequired[Project] 

37 from_file: NotRequired[bool] 

38 user: NotRequired[User] 

39 

40 

41def get_objects(request: AuthenticatedHttpRequest, kwargs) -> ObjectsDict: 

42 if "project" in kwargs: 

43 return {"project": parse_path(request, [kwargs["project"]], (Project,))} 

44 if "manage" in kwargs: 

45 return {"from_file": True} 

46 return {"user": request.user} 

47 

48 

49def check_perm(user: User, permission: str, objects: ObjectsDict): 

50 if "project" in objects: 

51 return user.has_perm(permission, objects["project"]) 

52 if "user" in objects: 

53 # User can edit own translation memory 

54 return True 

55 if "from_file" in objects: 

56 return user.has_perm(permission) 

57 return False 

58 

59 

60@method_decorator(login_required, name="dispatch") 

61class MemoryFormView(ErrorFormView): 

62 def get_success_url(self): 

63 if "manage" in self.kwargs: 

64 return reverse("manage-memory") 

65 return reverse("memory", kwargs=self.kwargs) 

66 

67 def dispatch(self, request: AuthenticatedHttpRequest, *args, **kwargs): # type: ignore[override] 

68 self.objects = get_objects(request, kwargs) 

69 return super().dispatch(request, *args, **kwargs) 

70 

71 

72class DeleteView(MemoryFormView): 

73 form_class = DeleteForm 

74 request: AuthenticatedHttpRequest 

75 

76 def form_valid(self, form): 

77 if not check_perm(self.request.user, "memory.delete", self.objects): 

78 raise PermissionDenied 

79 entries = Memory.objects.filter_type(**self.objects) 

80 if "origin" in self.request.POST: 

81 entries = entries.filter(origin=self.request.POST["origin"]) 

82 entries.using("default").delete() 

83 messages.success(self.request, gettext("Entries were deleted.")) 

84 return super().form_valid(form) 

85 

86 

87class RebuildView(MemoryFormView): 

88 form_class = DeleteForm 

89 request: AuthenticatedHttpRequest 

90 

91 def form_valid(self, form): 

92 if ( 

93 not check_perm(self.request.user, "memory.delete", self.objects) 

94 or "project" not in self.objects 

95 ): 

96 raise PermissionDenied 

97 origin = self.request.POST.get("origin") 

98 project = self.objects["project"] 

99 component_id = None 

100 if origin: 

101 try: 

102 component_id = project.component_set.get_by_path(origin).id 

103 except ObjectDoesNotExist as error: 

104 raise PermissionDenied from error 

105 # Delete private entries 

106 entries = Memory.objects.filter_type(**self.objects) 

107 if origin: 

108 entries = entries.filter(origin=origin) 

109 entries.using("default").delete() 

110 # Delete possible shared entries 

111 if origin: 

112 slugs = [origin] 

113 else: 

114 slugs = [ 

115 component.full_slug for component in project.component_set.prefetch() 

116 ] 

117 Memory.objects.filter(origin__in=slugs, shared=True).using("default").delete() 

118 # Rebuild memory in background 

119 import_memory.delay(project_id=project.id, component_id=component_id) 

120 messages.success( 

121 self.request, 

122 gettext( 

123 "Entries were deleted and the translation memory will be " 

124 "rebuilt in the background." 

125 ), 

126 ) 

127 return super().form_valid(form) 

128 

129 

130class UploadView(MemoryFormView): 

131 form_class = UploadForm 

132 request: AuthenticatedHttpRequest 

133 

134 def form_valid(self, form): 

135 if not check_perm(self.request.user, "memory.edit", self.objects): 

136 raise PermissionDenied 

137 try: 

138 Memory.objects.import_file( 

139 self.request, 

140 form.cleaned_data["file"], 

141 source_language=form.cleaned_data["source_language"], 

142 target_language=form.cleaned_data["target_language"], 

143 **self.objects, 

144 ) 

145 messages.success( 

146 self.request, 

147 gettext("File processed, the entries will appear shortly."), 

148 ) 

149 except MemoryImportError as error: 

150 messages.error(self.request, str(error)) 

151 return super().form_valid(form) 

152 

153 

154@method_decorator(login_required, name="dispatch") 

155class MemoryView(TemplateView): 

156 template_name = "memory/index.html" 

157 request: AuthenticatedHttpRequest 

158 objects: ObjectsDict 

159 

160 def dispatch(self, request: AuthenticatedHttpRequest, *args, **kwargs): # type: ignore[override] 

161 self.objects = get_objects(request, kwargs) 

162 return super().dispatch(request, *args, **kwargs) 

163 

164 def get_url(self, name): 

165 if "manage" in self.kwargs: 

166 return reverse(f"manage-{name}") 

167 return reverse(name, kwargs=self.kwargs) 

168 

169 @cached_property 

170 def entries(self): 

171 return Memory.objects.filter_type(**self.objects) 

172 

173 def get_origins(self): 

174 def get_url(slug: str) -> str: 

175 if "/" not in slug: 

176 return "" 

177 return reverse("show", kwargs={"path": slug.split("/")}) 

178 

179 from_file = list( 

180 self.entries.filter(from_file=True) 

181 .values("origin") 

182 .order_by("origin") 

183 .annotate(Count("id")) 

184 ) 

185 result = list( 

186 self.entries.filter(from_file=False) 

187 .values("origin") 

188 .order_by("origin") 

189 .annotate(Count("id")) 

190 ) 

191 for entry in result: 

192 entry["url"] = get_url(entry["origin"]) 

193 if "project" in self.objects: 

194 slugs = { 

195 component.full_slug 

196 for component in self.objects["project"].component_set.prefetch() 

197 } 

198 existing = {entry["origin"] for entry in result} 

199 for entry in result: 

200 entry["can_rebuild"] = entry["origin"] in slugs 

201 # Add missing ones 

202 result.extend( 

203 { 

204 "origin": missing, 

205 "id__count": 0, 

206 "can_rebuild": True, 

207 "url": get_url(missing), 

208 } 

209 for missing in slugs - existing 

210 ) 

211 return from_file + result 

212 

213 def get_languages(self): 

214 if "manage" in self.kwargs: 

215 return [] 

216 results = ( 

217 self.entries.values("source_language", "target_language") 

218 .order_by("source_language__code", "target_language__code") 

219 .annotate(Count("id")) 

220 ) 

221 languages = { 

222 language.id: language 

223 for language in Language.objects.filter( 

224 pk__in={result["source_language"] for result in results} 

225 | {result["target_language"] for result in results} 

226 ) 

227 } 

228 

229 return [ 

230 { 

231 "source_language": languages[result["source_language"]], 

232 "target_language": languages[result["target_language"]], 

233 "id__count": result["id__count"], 

234 } 

235 for result in results 

236 ] 

237 

238 def get_context_data(self, **kwargs): 

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

240 context.update(self.objects) 

241 context["num_entries"] = self.entries.count() 

242 context["entries_origin"] = self.get_origins() 

243 context["entries_languages"] = self.get_languages() 

244 context["total_entries"] = Metric.objects.get_current_metric( 

245 None, Metric.SCOPE_GLOBAL, 0 

246 )["memory"] 

247 context["upload_url"] = self.get_url("memory-upload") 

248 context["download_url"] = self.get_url("memory-download") 

249 user = self.request.user 

250 if check_perm(user, "memory.delete", self.objects): 

251 context["delete_url"] = self.get_url("memory-delete") 

252 if "project" in self.objects: 

253 context["rebuild_url"] = self.get_url("memory-rebuild") 

254 if check_perm(user, "memory.edit", self.objects): 

255 context["upload_form"] = UploadForm() 

256 if "from_file" in self.objects: 

257 context["menu_items"] = MENU 

258 context["menu_page"] = "memory" 

259 if "from_file" in self.objects or ( 

260 "project" in self.objects and self.objects["project"].use_shared_tm 

261 ): 

262 context["shared_entries"] = Memory.objects.filter(shared=True).count() 

263 return context 

264 

265 

266class DownloadView(MemoryView): 

267 def get(self, request: AuthenticatedHttpRequest, *args, **kwargs): # type: ignore[override] 

268 fmt = request.GET.get("format", "json") 

269 data = Memory.objects.filter_type(**self.objects).prefetch_lang() 

270 if "origin" in request.GET: 

271 data = data.filter(origin=request.GET["origin"]) 

272 if "source_language" in request.GET: 

273 data = data.filter(source_language_id=request.GET["source_language"]) 

274 if "target_language" in request.GET: 

275 data = data.filter(target_language_id=request.GET["target_language"]) 

276 if "from_file" in self.objects and "kind" in request.GET: 

277 if request.GET["kind"] == "shared": 

278 data = Memory.objects.filter_type(use_shared=True).prefetch_lang() 

279 elif request.GET["kind"] == "all": 

280 data = Memory.objects.prefetch_lang() 

281 if fmt == "tmx": 

282 response = render( 

283 request, 

284 "memory/dump.tmx", 

285 {"data": data}, 

286 content_type="application/x-tmx", 

287 ) 

288 else: 

289 fmt = "json" 

290 response = JsonResponse([item.as_dict() for item in data], safe=False) 

291 response["Content-Disposition"] = CD_TEMPLATE.format(fmt) 

292 return response