Coverage for app/venv/lib/python3.14/site-packages/weblate/trans/views/acl.py: 27%

150 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 datetime import timedelta 

7from itertools import chain 

8from typing import TYPE_CHECKING 

9 

10from django.conf import settings 

11from django.contrib.auth.decorators import login_required 

12from django.core.exceptions import PermissionDenied 

13from django.db.models import Count, Prefetch 

14from django.shortcuts import redirect 

15from django.template.loader import render_to_string 

16from django.utils import timezone 

17from django.utils.translation import gettext 

18from django.views.decorators.http import require_POST 

19 

20from weblate.accounts.models import AuditLog 

21from weblate.accounts.utils import remove_user 

22from weblate.auth.data import SELECTION_ALL 

23from weblate.auth.forms import InviteEmailForm, InviteUserForm, ProjectTeamForm 

24from weblate.auth.models import Invitation, User 

25from weblate.trans.actions import ActionEvents 

26from weblate.trans.forms import ( 

27 ProjectTokenCreateForm, 

28 ProjectUserGroupForm, 

29 UserBlockForm, 

30 UserManageForm, 

31) 

32from weblate.trans.models import Project 

33from weblate.trans.util import redirect_param, render 

34from weblate.utils import messages 

35from weblate.utils.views import parse_path, show_form_errors 

36from weblate.vcs.ssh import get_all_key_data 

37 

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

39 from weblate.auth.models import AuthenticatedHttpRequest 

40 

41 

42def check_user_form( 

43 request, project, form_class=UserManageForm, pass_project: bool = False 

44): 

45 """ 

46 Check project permission and UserManageForm. 

47 

48 This is simple helper to perform needed validation for all user management views. 

49 """ 

50 obj = parse_path(request, [project], (Project,)) 

51 

52 if not request.user.has_perm("project.permissions", obj): 

53 raise PermissionDenied 

54 

55 kwargs = {} 

56 if pass_project: 

57 kwargs["project"] = obj 

58 

59 form = form_class(data=request.POST, **kwargs) 

60 

61 if form.is_valid(): 

62 return obj, form 

63 show_form_errors(request, form) 

64 return obj, None 

65 

66 

67@require_POST 

68@login_required 

69def set_groups(request: AuthenticatedHttpRequest, project): 

70 """Change group assignment for a user.""" 

71 obj, form = check_user_form( 

72 request, project, form_class=ProjectUserGroupForm, pass_project=True 

73 ) 

74 

75 user = form.cleaned_data["user"] 

76 desired_groups = {group.id for group in form.cleaned_data["groups"]} 

77 current_groups = set( 

78 user.groups.filter(defining_project=obj).values_list("id", flat=True) 

79 ) 

80 

81 for group in obj.defined_groups.all(): 

82 if group.id in desired_groups: 

83 if group.id in current_groups: 

84 continue 

85 user.add_team(request, group) 

86 obj.change_set.create( 

87 action=ActionEvents.ADD_USER, 

88 user=request.user, 

89 details={"username": user.username, "group": group.name}, 

90 ) 

91 elif group.id in current_groups: 

92 if request.user == user: 

93 messages.error(request, gettext("You can not remove yourself!")) 

94 continue 

95 user.remove_team(request, group) 

96 obj.change_set.create( 

97 action=ActionEvents.REMOVE_USER, 

98 user=request.user, 

99 details={"username": user.username, "group": group.name}, 

100 ) 

101 

102 return redirect_param( 

103 "manage-access", "#api" if user.is_bot else "", project=obj.slug 

104 ) 

105 

106 

107@require_POST 

108@login_required 

109def add_user(request: AuthenticatedHttpRequest, project): 

110 """Add user to a project.""" 

111 obj, form = check_user_form( 

112 request, project, form_class=InviteUserForm, pass_project=True 

113 ) 

114 

115 if form is not None: 

116 form.save(request) 

117 

118 return redirect("manage-access", project=obj.slug) 

119 

120 

121@require_POST 

122@login_required 

123def block_user(request: AuthenticatedHttpRequest, project): 

124 """Block user from a project.""" 

125 obj, form = check_user_form(request, project, form_class=UserBlockForm) 

126 

127 if form is not None and form.cleaned_data["user"].id == request.user.id: 

128 messages.error(request, gettext("You can not block yourself on this project.")) 

129 elif form is not None: 

130 user = form.cleaned_data["user"] 

131 

132 if form.cleaned_data.get("expiry"): 

133 expiry = timezone.now() + timedelta(days=int(form.cleaned_data["expiry"])) 

134 else: 

135 expiry = None 

136 _userblock, created = user.userblock_set.get_or_create( 

137 project=obj, defaults={"expiry": expiry} 

138 ) 

139 if created: 

140 AuditLog.objects.create( 

141 user, 

142 None, 

143 "blocked", 

144 project=obj.name, 

145 username=request.user.username, 

146 expiry=expiry.isoformat() if expiry else None, 

147 ) 

148 messages.success(request, gettext("User has been blocked on this project.")) 

149 else: 

150 messages.error(request, gettext("User is already blocked on this project.")) 

151 

152 return redirect("manage-access", project=obj.slug) 

153 

154 

155@require_POST 

156@login_required 

157def unblock_user(request: AuthenticatedHttpRequest, project): 

158 """Block user from a project.""" 

159 obj, form = check_user_form( 

160 request, 

161 project, 

162 ) 

163 

164 if form is not None: 

165 user = form.cleaned_data["user"] 

166 user.userblock_set.filter(project=obj).delete() 

167 

168 return redirect("manage-access", project=obj.slug) 

169 

170 

171@require_POST 

172@login_required 

173def invite_user(request: AuthenticatedHttpRequest, project): 

174 """Invite user to a project.""" 

175 obj, form = check_user_form( 

176 request, project, form_class=InviteEmailForm, pass_project=True 

177 ) 

178 

179 if form is not None: 

180 form.save(request, obj) 

181 

182 return redirect("manage-access", project=obj.slug) 

183 

184 

185@require_POST 

186@login_required 

187def delete_user(request: AuthenticatedHttpRequest, project): 

188 """Remove user from a project.""" 

189 obj, form = check_user_form( 

190 request, 

191 project, 

192 ) 

193 redirect_url = "" 

194 

195 if form is not None: 

196 user = form.cleaned_data["user"] 

197 if request.user == user: 

198 messages.error(request, gettext("You can not remove yourself!")) 

199 else: 

200 if user.is_bot: 

201 redirect_url = "#api" 

202 remove_user(user, request) 

203 else: 

204 obj.remove_user(user) 

205 obj.change_set.create( 

206 action=ActionEvents.REMOVE_USER, 

207 user=request.user, 

208 details={"username": user.username}, 

209 ) 

210 if user.is_bot: 

211 messages.success( 

212 request, gettext("Token has been removed from this project.") 

213 ) 

214 else: 

215 messages.success( 

216 request, gettext("User has been removed from this project.") 

217 ) 

218 

219 return redirect_param("manage-access", redirect_url, project=obj.slug) 

220 

221 

222@login_required 

223def manage_access(request: AuthenticatedHttpRequest, project): 

224 """User management view.""" 

225 obj = parse_path(request, [project], (Project,)) 

226 

227 if not request.user.has_perm("project.permissions", obj): 

228 raise PermissionDenied 

229 

230 groups = ( 

231 obj.defined_groups.order() 

232 .annotate(Count("user"), Count("autogroup")) 

233 .prefetch_related("languages", "components") 

234 ) 

235 users = ( 

236 User.objects.filter(groups__in=groups, is_bot=False) 

237 .distinct() 

238 .order() 

239 .prefetch_related( 

240 Prefetch( 

241 "groups", 

242 queryset=groups, 

243 to_attr="project_groups", 

244 ), 

245 ) 

246 ) 

247 project_tokens = ( 

248 User.objects.filter(groups__in=groups, is_bot=True) 

249 .distinct() 

250 .order() 

251 .prefetch_related( 

252 Prefetch( 

253 "groups", 

254 queryset=groups, 

255 to_attr="project_groups", 

256 ), 

257 ) 

258 ) 

259 

260 for user in chain(users, project_tokens): 

261 user.group_edit_form = ProjectUserGroupForm( 

262 obj, 

263 initial={"user": user.username, "groups": user.project_groups}, 

264 auto_id=f"id_user_{user.id}_%s", 

265 ) 

266 

267 return render( 

268 request, 

269 "trans/project-access.html", 

270 { 

271 "object": obj, 

272 "project": obj, 

273 "project_tokens": project_tokens, 

274 "groups": groups, 

275 "all_users": users, 

276 "blocked_users": obj.userblock_set.select_related("user"), 

277 "invitations": Invitation.objects.filter( 

278 group__defining_project=obj 

279 ).select_related("user"), 

280 "create_project_token_form": ProjectTokenCreateForm(obj), 

281 "create_team_form": ProjectTeamForm( 

282 project=obj, initial={"language_selection": SELECTION_ALL} 

283 ), 

284 "block_user_form": UserBlockForm( 

285 initial={"user": request.GET.get("block_user")} 

286 ), 

287 "invite_user_form": InviteUserForm(project=obj), 

288 "invite_email_form": InviteEmailForm(project=obj) 

289 if settings.REGISTRATION_OPEN 

290 else None, 

291 "public_ssh_keys": get_all_key_data(), 

292 }, 

293 ) 

294 

295 

296@require_POST 

297@login_required 

298def create_token(request: AuthenticatedHttpRequest, project): 

299 """Create project token.""" 

300 obj = parse_path(request, [project], (Project,)) 

301 

302 if not request.user.has_perm("project.permissions", obj): 

303 raise PermissionDenied 

304 

305 form = ProjectTokenCreateForm(obj, request.POST) 

306 

307 if form.is_valid(): 

308 token = form.save() 

309 messages.info( 

310 request, 

311 render_to_string( 

312 "trans/projecttoken-created.html", {"token": token.auth_token.key} 

313 ), 

314 ) 

315 else: 

316 show_form_errors(request, form) 

317 

318 return redirect_param("manage-access", "#api", project=obj.slug) 

319 

320 

321@require_POST 

322@login_required 

323def create_group(request: AuthenticatedHttpRequest, project): 

324 """Delete project group.""" 

325 obj = parse_path(request, [project], (Project,)) 

326 

327 if not request.user.has_perm("project.permissions", obj): 

328 raise PermissionDenied 

329 

330 form = ProjectTeamForm(obj, request.POST) 

331 

332 if form.is_valid(): 

333 form.save(project=obj) 

334 else: 

335 show_form_errors(request, form) 

336 

337 return redirect_param("manage-access", "#teams", project=obj.slug)