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
« 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 datetime import timedelta
7from itertools import chain
8from typing import TYPE_CHECKING
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
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
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
42def check_user_form(
43 request, project, form_class=UserManageForm, pass_project: bool = False
44):
45 """
46 Check project permission and UserManageForm.
48 This is simple helper to perform needed validation for all user management views.
49 """
50 obj = parse_path(request, [project], (Project,))
52 if not request.user.has_perm("project.permissions", obj):
53 raise PermissionDenied
55 kwargs = {}
56 if pass_project:
57 kwargs["project"] = obj
59 form = form_class(data=request.POST, **kwargs)
61 if form.is_valid():
62 return obj, form
63 show_form_errors(request, form)
64 return obj, None
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 )
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 )
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 )
102 return redirect_param(
103 "manage-access", "#api" if user.is_bot else "", project=obj.slug
104 )
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 )
115 if form is not None:
116 form.save(request)
118 return redirect("manage-access", project=obj.slug)
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)
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"]
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."))
152 return redirect("manage-access", project=obj.slug)
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 )
164 if form is not None:
165 user = form.cleaned_data["user"]
166 user.userblock_set.filter(project=obj).delete()
168 return redirect("manage-access", project=obj.slug)
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 )
179 if form is not None:
180 form.save(request, obj)
182 return redirect("manage-access", project=obj.slug)
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 = ""
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 )
219 return redirect_param("manage-access", redirect_url, project=obj.slug)
222@login_required
223def manage_access(request: AuthenticatedHttpRequest, project):
224 """User management view."""
225 obj = parse_path(request, [project], (Project,))
227 if not request.user.has_perm("project.permissions", obj):
228 raise PermissionDenied
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 )
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 )
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 )
296@require_POST
297@login_required
298def create_token(request: AuthenticatedHttpRequest, project):
299 """Create project token."""
300 obj = parse_path(request, [project], (Project,))
302 if not request.user.has_perm("project.permissions", obj):
303 raise PermissionDenied
305 form = ProjectTokenCreateForm(obj, request.POST)
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)
318 return redirect_param("manage-access", "#api", project=obj.slug)
321@require_POST
322@login_required
323def create_group(request: AuthenticatedHttpRequest, project):
324 """Delete project group."""
325 obj = parse_path(request, [project], (Project,))
327 if not request.user.has_perm("project.permissions", obj):
328 raise PermissionDenied
330 form = ProjectTeamForm(obj, request.POST)
332 if form.is_valid():
333 form.save(project=obj)
334 else:
335 show_form_errors(request, form)
337 return redirect_param("manage-access", "#teams", project=obj.slug)