Coverage for app/venv/lib/python3.14/site-packages/weblate/auth/admin.py: 44%
165 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
5from __future__ import annotations
7from typing import TYPE_CHECKING
9from django import forms
10from django.contrib import admin
11from django.contrib.auth.admin import UserAdmin
12from django.contrib.auth.forms import UserChangeForm, UserCreationForm
13from django.core.exceptions import ValidationError
14from django.utils.translation import gettext, gettext_lazy
16from weblate.accounts.forms import FullNameField, UniqueEmailMixin, UniqueUsernameField
17from weblate.accounts.utils import remove_user
18from weblate.auth.data import ROLES
19from weblate.auth.models import AutoGroup, Group, Role, User
20from weblate.wladmin.models import WeblateModelAdmin
22if TYPE_CHECKING: 22 ↛ 23line 22 didn't jump to line 23 because the condition on line 22 was never true
23 from weblate.auth.models import AuthenticatedHttpRequest
25BUILT_IN_ROLES = {role[0] for role in ROLES}
28def block_group_edit(obj: Group):
29 """Whether to allow user editing of a group."""
30 return obj and obj.internal
33def block_role_edit(obj: Role):
34 return obj and obj.name in BUILT_IN_ROLES
37class AutoGroupChangeForm(forms.ModelForm):
38 class Meta:
39 model = AutoGroup
40 fields = "__all__" # noqa: DJ007
42 def has_changed(self) -> bool:
43 """
44 Check whether data differs from initial.
46 By always returning true even unchanged inlines will get validated and saved.
47 """
48 return True
51class InlineAutoGroupAdmin(admin.TabularInline):
52 model = AutoGroup
53 form = AutoGroupChangeForm
54 extra = 0
56 def has_add_permission(self, request: AuthenticatedHttpRequest, obj=None):
57 if block_group_edit(obj):
58 return False
59 return super().has_add_permission(request, obj)
61 def has_change_permission(self, request: AuthenticatedHttpRequest, obj=None):
62 if block_group_edit(obj):
63 return False
64 return super().has_change_permission(request, obj)
66 def has_delete_permission(self, request: AuthenticatedHttpRequest, obj=None):
67 if block_group_edit(obj):
68 return False
69 return super().has_delete_permission(request, obj)
72@admin.register(Role)
73class RoleAdmin(WeblateModelAdmin):
74 list_display = ("name",)
75 filter_horizontal = ("permissions",)
77 def has_change_permission(self, request: AuthenticatedHttpRequest, obj=None):
78 if block_role_edit(obj):
79 return False
80 return super().has_change_permission(request, obj)
82 def has_delete_permission(self, request: AuthenticatedHttpRequest, obj=None):
83 if block_role_edit(obj):
84 return False
85 return super().has_delete_permission(request, obj)
88class WeblateUserChangeForm(UserChangeForm):
89 class Meta:
90 model = User
91 fields = "__all__"
92 field_classes = { # noqa: RUF012
93 "username": UniqueUsernameField,
94 "full_name": FullNameField,
95 }
97 def __init__(self, *args, **kwargs) -> None:
98 super().__init__(*args, **kwargs)
99 self.fields["email"].required = True
100 self.fields["username"].valid = self.instance.username
103class WeblateUserCreationForm(UserCreationForm, UniqueEmailMixin):
104 validate_unique_mail = True
106 class Meta:
107 model = User
108 fields = ("username", "email", "full_name")
109 field_classes = { # noqa: RUF012
110 "username": UniqueUsernameField,
111 "full_name": FullNameField,
112 }
114 def __init__(self, *args, **kwargs) -> None:
115 super().__init__(*args, **kwargs)
116 self.fields["email"].required = True
119class WeblateAuthAdmin(WeblateModelAdmin):
120 def get_deleted_objects(self, objs, request: AuthenticatedHttpRequest):
121 (
122 deleted_objects,
123 model_count,
124 perms_needed,
125 protected,
126 ) = super().get_deleted_objects(objs, request)
127 # Discard permission check for objects where deletion in admin is disabled
128 # This behaves differently depending on Django version
129 perms_needed.discard("profile")
130 perms_needed.discard("User profile")
131 perms_needed.discard("audit_log")
132 perms_needed.discard("audit log")
133 perms_needed.discard("Audit log entry")
134 perms_needed.discard("verified_email")
135 perms_needed.discard("verified email")
136 perms_needed.discard("verified e-mail")
137 perms_needed.discard("Verified email")
138 perms_needed.discard("Verified e-mail")
139 return deleted_objects, model_count, perms_needed, protected
142@admin.register(User)
143class WeblateUserAdmin(WeblateAuthAdmin, UserAdmin):
144 """
145 Custom UserAdmin class.
147 Used to add listing of group membership and whether a user is active.
148 """
150 list_display = (
151 "username",
152 "email",
153 "full_name",
154 "user_groups",
155 "is_active",
156 "is_bot",
157 "is_superuser",
158 )
159 search_fields = ("username", "full_name", "email")
160 form = WeblateUserChangeForm
161 add_form = WeblateUserCreationForm
162 add_fieldsets = (
163 (None, {"fields": ("username",)}),
164 (gettext_lazy("Personal info"), {"fields": ("full_name", "email")}),
165 (gettext_lazy("Authentication"), {"fields": ("password1", "password2")}),
166 )
167 fieldsets = (
168 (None, {"fields": ("username", "password")}),
169 (gettext_lazy("Personal info"), {"fields": ("full_name", "email")}),
170 (
171 gettext_lazy("Permissions"),
172 {"fields": ("is_active", "is_bot", "is_superuser", "groups")},
173 ),
174 (
175 gettext_lazy("Important dates"),
176 {"fields": ("last_login", "date_joined", "date_expires")},
177 ),
178 )
179 list_filter = ("is_superuser", "is_active", "is_bot", "groups")
180 filter_horizontal = ("groups",)
182 def user_groups(self, obj):
183 """Display comma separated list of user groups."""
184 return ",".join(obj.groups.values_list("name", flat=True))
186 def action_checkbox(self, obj):
187 if obj.is_anonymous:
188 return ""
189 return super().action_checkbox(obj)
191 def has_delete_permission(self, request: AuthenticatedHttpRequest, obj=None):
192 if obj and obj.is_anonymous:
193 return False
194 return super().has_delete_permission(request, obj)
196 def delete_model(self, request: AuthenticatedHttpRequest, obj) -> None:
197 """Given a model instance delete it from the database."""
198 remove_user(obj, request)
200 def delete_queryset(self, request: AuthenticatedHttpRequest, queryset) -> None:
201 """Given a queryset, delete it from the database."""
202 for obj in queryset.iterator():
203 self.delete_model(request, obj)
206class GroupChangeForm(forms.ModelForm):
207 class Meta:
208 model = Group
209 fields = "__all__" # noqa: DJ007
211 def __init__(self, *args, **kwargs) -> None:
212 super().__init__(*args, **kwargs)
213 if "components" in self.fields:
214 components = self.fields["components"]
215 components.queryset = components.queryset.select_related("project")
217 def clean(self) -> None:
218 super().clean()
219 has_componentlist = bool(self.cleaned_data["componentlists"])
220 has_project = bool(self.cleaned_data["projects"])
221 has_component = bool(self.cleaned_data["components"])
222 if has_componentlist:
223 fields = []
224 if has_project:
225 fields.append("projects")
226 if has_component:
227 fields.append("components")
228 if fields:
229 raise ValidationError(
230 {
231 field: gettext(
232 "This is not used when a component list is selected."
233 )
234 for field in fields
235 }
236 )
237 elif has_component and has_project:
238 raise ValidationError(
239 {"projects": gettext("This is not used when a component is selected.")}
240 )
243@admin.register(Group)
244class WeblateGroupAdmin(WeblateAuthAdmin):
245 save_as = True
246 model = Group
247 form = GroupChangeForm
248 inlines = (InlineAutoGroupAdmin,)
249 search_fields = ("name", "defining_project__name")
250 ordering = ("defining_project__name", "name")
251 list_filter = ("internal", "project_selection", "language_selection")
252 filter_horizontal = (
253 "roles",
254 "projects",
255 "languages",
256 "components",
257 "componentlists",
258 )
259 list_display = ("name", "defining_project")
261 new_obj: Group
263 def action_checkbox(self, obj):
264 if obj.internal:
265 return ""
266 return super().action_checkbox(obj)
268 def has_delete_permission(self, request: AuthenticatedHttpRequest, obj=None):
269 if obj and obj.internal:
270 return False
271 return super().has_delete_permission(request, obj)
273 def has_change_permission(self, request: AuthenticatedHttpRequest, obj=None):
274 if block_group_edit(obj):
275 return False
276 return super().has_change_permission(request, obj)
278 def save_model(self, request: AuthenticatedHttpRequest, obj, form, change) -> None:
279 """
280 Fix saving of automatic language/project selection, part 1.
282 Stores saved object as an attribute to be used by save_related.
283 """
284 super().save_model(request, obj, form, change)
285 self.new_obj = obj
287 def save_related(
288 self, request: AuthenticatedHttpRequest, form, formsets, change
289 ) -> None:
290 """
291 Fix saving of automatic language/project selection, part 2.
293 Uses stored attribute to save the model again. Saving triggers the automation
294 and adjusts project/language selection according to the chosen value.
295 """
296 super().save_related(request, form, formsets, change)
297 self.new_obj.save()