Coverage for app/venv/lib/python3.14/site-packages/weblate/trans/views/search.py: 24%
102 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
8from django.contrib.auth.decorators import login_required
9from django.core.exceptions import PermissionDenied
10from django.db import transaction
11from django.db.models import Count, Sum
12from django.shortcuts import redirect
13from django.utils.translation import gettext, ngettext
14from django.views.decorators.cache import never_cache
15from django.views.decorators.http import require_POST
17from weblate.lang.models import Language
18from weblate.trans.actions import ActionEvents
19from weblate.trans.bulk import bulk_perform
20from weblate.trans.forms import (
21 BulkEditForm,
22 ReplaceConfirmForm,
23 ReplaceForm,
24 SearchForm,
25)
26from weblate.trans.models import Category, Component, Project, Translation, Unit
27from weblate.trans.models.unit import fill_in_source_translation
28from weblate.trans.util import render
29from weblate.utils import messages
30from weblate.utils.ratelimit import check_rate_limit
31from weblate.utils.stats import CategoryLanguage, ProjectLanguage
32from weblate.utils.views import (
33 get_paginator,
34 import_message,
35 parse_path_units,
36 show_form_errors,
37)
39if TYPE_CHECKING: 39 ↛ 40line 39 didn't jump to line 40 because the condition on line 39 was never true
40 from weblate.auth.models import AuthenticatedHttpRequest
43@login_required
44@require_POST
45def search_replace(request: AuthenticatedHttpRequest, path):
46 obj, unit_set, context = parse_path_units(
47 request,
48 path,
49 (Translation, Component, Project, ProjectLanguage, Category, CategoryLanguage),
50 )
52 if not request.user.has_perm("unit.edit", obj):
53 raise PermissionDenied
55 form = ReplaceForm(obj=obj, data=request.POST)
57 if not form.is_valid():
58 messages.error(request, gettext("Could not process form!"))
59 show_form_errors(request, form)
60 return redirect(obj)
62 search_text = form.cleaned_data["search"]
63 replacement = form.cleaned_data["replacement"]
64 query = form.cleaned_data.get("q")
66 matching = unit_set.filter(target__contains=search_text)
67 if query:
68 matching = matching.search(query)
70 updated = 0
72 matching_ids = list(matching.order_by("id").values_list("id", flat=True)[:251])
74 if matching_ids:
75 if len(matching_ids) == 251:
76 matching_ids = matching_ids[:250]
77 limited = True
78 else:
79 limited = False
81 matching = Unit.objects.filter(id__in=matching_ids).prefetch()
83 confirm = ReplaceConfirmForm(matching, request.POST)
85 if not confirm.is_valid():
86 for unit in matching:
87 # This is rendered using format_unit_target which does split_plurals
88 unit.replacement = unit.target.replace(search_text, replacement)
89 context.update(
90 {
91 "matching": matching,
92 "search_query": search_text,
93 "replacement": replacement,
94 "form": form,
95 "limited": limited,
96 "confirm": ReplaceConfirmForm(matching),
97 }
98 )
99 return render(request, "replace.html", context)
101 matching = confirm.cleaned_data["units"]
103 with transaction.atomic():
104 for unit in matching.select_for_update():
105 if not request.user.has_perm("unit.edit", unit):
106 continue
107 unit.translate(
108 request.user,
109 [
110 plural.replace(search_text, replacement)
111 for plural in unit.get_target_plurals()
112 ],
113 unit.state,
114 change_action=ActionEvents.REPLACE,
115 )
116 updated += 1
118 import_message(
119 request,
120 updated,
121 gettext("Search and replace completed, no strings were updated."),
122 ngettext(
123 "Search and replace completed, %d string was updated.",
124 "Search and replace completed, %d strings were updated.",
125 updated,
126 ),
127 )
129 return redirect(obj)
132@never_cache
133def search(request: AuthenticatedHttpRequest, path=None):
134 """Perform site-wide search on units."""
135 is_ratelimited = not check_rate_limit("search", request)
136 obj, unit_set, context = parse_path_units(
137 request,
138 path,
139 (
140 Component,
141 Project,
142 ProjectLanguage,
143 Translation,
144 Category,
145 CategoryLanguage,
146 Language,
147 None,
148 ),
149 )
151 search_form = SearchForm(request=request, data=request.GET, obj=obj)
152 context["search_form"] = search_form
153 context["back_url"] = obj.get_absolute_url() if obj is not None else None
154 context["bootstrap_5"] = True
156 if not is_ratelimited and request.GET and search_form.is_valid():
157 # This is ugly way to hide query builder when showing results
158 search_form = SearchForm(
159 request=request, data=request.GET, show_builder=False, obj=obj
160 )
161 search_form.is_valid()
162 units = unit_set.prefetch_bulk().search(
163 search_form.cleaned_data.get("q", ""), project=context.get("project")
164 )
166 # Count total strings and sum total words from the search results
167 aggregation = units.aggregate(
168 total_strings=Count("id"), total_words=Sum("num_words")
169 )
170 # Get the total strings and total words from the aggregation
171 total_strings = aggregation["total_strings"]
172 total_words = aggregation["total_words"]
174 units = get_paginator(
175 request, units.order_by_request(search_form.cleaned_data, obj)
176 )
177 # Make sure source translation is available
178 fill_in_source_translation(units)
179 # Rebuild context from scratch here to get new form
180 context.update(
181 {
182 "search_form": search_form,
183 "show_results": True,
184 "page_obj": units,
185 "path_object": obj,
186 "title": gettext("Search for %s") % (search_form.cleaned_data["q"]),
187 "query_string": search_form.urlencode(),
188 "search_url": search_form.urlencode(),
189 "search_query": search_form.cleaned_data["q"],
190 "search_items": search_form.items(),
191 "total_strings": total_strings,
192 "total_words": total_words,
193 }
194 )
195 elif is_ratelimited:
196 messages.error(
197 request, gettext("Too many search queries, please try again later.")
198 )
199 elif request.GET:
200 messages.error(request, gettext("Invalid search query!"))
201 show_form_errors(request, search_form)
203 return render(request, "search.html", context)
206@login_required
207@require_POST
208@never_cache
209def bulk_edit(request: AuthenticatedHttpRequest, path):
210 obj, unit_set, context = parse_path_units(
211 request,
212 path,
213 (Translation, Component, Project, ProjectLanguage, Category, CategoryLanguage),
214 )
216 if not request.user.has_perm("unit.bulk_edit", obj) or not request.user.has_perm(
217 "unit.edit", obj
218 ):
219 raise PermissionDenied
221 form = BulkEditForm(request.user, obj, request.POST, project=context["project"])
223 if not form.is_valid():
224 messages.error(request, gettext("Could not process form!"))
225 show_form_errors(request, form)
226 return redirect(obj)
228 updated = bulk_perform(
229 request.user,
230 unit_set,
231 query=form.cleaned_data["q"],
232 target_state=form.cleaned_data["state"],
233 add_flags=form.cleaned_data["add_flags"],
234 remove_flags=form.cleaned_data["remove_flags"],
235 add_labels=form.cleaned_data["add_labels"],
236 remove_labels=form.cleaned_data["remove_labels"],
237 project=context["project"],
238 components=context["components"],
239 )
241 import_message(
242 request,
243 updated,
244 gettext("Bulk edit completed, no strings were updated."),
245 ngettext(
246 "Bulk edit completed, %d string was updated.",
247 "Bulk edit completed, %d strings were updated.",
248 updated,
249 ),
250 )
252 return redirect(obj)