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

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.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 

16 

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) 

38 

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 

41 

42 

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 ) 

51 

52 if not request.user.has_perm("unit.edit", obj): 

53 raise PermissionDenied 

54 

55 form = ReplaceForm(obj=obj, data=request.POST) 

56 

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) 

61 

62 search_text = form.cleaned_data["search"] 

63 replacement = form.cleaned_data["replacement"] 

64 query = form.cleaned_data.get("q") 

65 

66 matching = unit_set.filter(target__contains=search_text) 

67 if query: 

68 matching = matching.search(query) 

69 

70 updated = 0 

71 

72 matching_ids = list(matching.order_by("id").values_list("id", flat=True)[:251]) 

73 

74 if matching_ids: 

75 if len(matching_ids) == 251: 

76 matching_ids = matching_ids[:250] 

77 limited = True 

78 else: 

79 limited = False 

80 

81 matching = Unit.objects.filter(id__in=matching_ids).prefetch() 

82 

83 confirm = ReplaceConfirmForm(matching, request.POST) 

84 

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) 

100 

101 matching = confirm.cleaned_data["units"] 

102 

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 

117 

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 ) 

128 

129 return redirect(obj) 

130 

131 

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 ) 

150 

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 

155 

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 ) 

165 

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"] 

173 

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) 

202 

203 return render(request, "search.html", context) 

204 

205 

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 ) 

215 

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 

220 

221 form = BulkEditForm(request.user, obj, request.POST, project=context["project"]) 

222 

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) 

227 

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 ) 

240 

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 ) 

251 

252 return redirect(obj)