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
« 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
6from typing import TYPE_CHECKING, NotRequired, TypedDict
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
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
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
32CD_TEMPLATE = 'attachment; filename="weblate-memory.{}"'
35class ObjectsDict(TypedDict):
36 project: NotRequired[Project]
37 from_file: NotRequired[bool]
38 user: NotRequired[User]
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}
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
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)
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)
72class DeleteView(MemoryFormView):
73 form_class = DeleteForm
74 request: AuthenticatedHttpRequest
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)
87class RebuildView(MemoryFormView):
88 form_class = DeleteForm
89 request: AuthenticatedHttpRequest
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)
130class UploadView(MemoryFormView):
131 form_class = UploadForm
132 request: AuthenticatedHttpRequest
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)
154@method_decorator(login_required, name="dispatch")
155class MemoryView(TemplateView):
156 template_name = "memory/index.html"
157 request: AuthenticatedHttpRequest
158 objects: ObjectsDict
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)
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)
169 @cached_property
170 def entries(self):
171 return Memory.objects.filter_type(**self.objects)
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("/")})
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
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 }
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 ]
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
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