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

1# Copyright © Michal Čihař <michal@weblate.org> 

2# 

3# SPDX-License-Identifier: GPL-3.0-or-later 

4 

5from __future__ import annotations 

6 

7from typing import TYPE_CHECKING 

8 

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 

15 

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 

21 

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 

24 

25BUILT_IN_ROLES = {role[0] for role in ROLES} 

26 

27 

28def block_group_edit(obj: Group): 

29 """Whether to allow user editing of a group.""" 

30 return obj and obj.internal 

31 

32 

33def block_role_edit(obj: Role): 

34 return obj and obj.name in BUILT_IN_ROLES 

35 

36 

37class AutoGroupChangeForm(forms.ModelForm): 

38 class Meta: 

39 model = AutoGroup 

40 fields = "__all__" # noqa: DJ007 

41 

42 def has_changed(self) -> bool: 

43 """ 

44 Check whether data differs from initial. 

45 

46 By always returning true even unchanged inlines will get validated and saved. 

47 """ 

48 return True 

49 

50 

51class InlineAutoGroupAdmin(admin.TabularInline): 

52 model = AutoGroup 

53 form = AutoGroupChangeForm 

54 extra = 0 

55 

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) 

60 

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) 

65 

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) 

70 

71 

72@admin.register(Role) 

73class RoleAdmin(WeblateModelAdmin): 

74 list_display = ("name",) 

75 filter_horizontal = ("permissions",) 

76 

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) 

81 

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) 

86 

87 

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 } 

96 

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 

101 

102 

103class WeblateUserCreationForm(UserCreationForm, UniqueEmailMixin): 

104 validate_unique_mail = True 

105 

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 } 

113 

114 def __init__(self, *args, **kwargs) -> None: 

115 super().__init__(*args, **kwargs) 

116 self.fields["email"].required = True 

117 

118 

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 

140 

141 

142@admin.register(User) 

143class WeblateUserAdmin(WeblateAuthAdmin, UserAdmin): 

144 """ 

145 Custom UserAdmin class. 

146 

147 Used to add listing of group membership and whether a user is active. 

148 """ 

149 

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",) 

181 

182 def user_groups(self, obj): 

183 """Display comma separated list of user groups.""" 

184 return ",".join(obj.groups.values_list("name", flat=True)) 

185 

186 def action_checkbox(self, obj): 

187 if obj.is_anonymous: 

188 return "" 

189 return super().action_checkbox(obj) 

190 

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) 

195 

196 def delete_model(self, request: AuthenticatedHttpRequest, obj) -> None: 

197 """Given a model instance delete it from the database.""" 

198 remove_user(obj, request) 

199 

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) 

204 

205 

206class GroupChangeForm(forms.ModelForm): 

207 class Meta: 

208 model = Group 

209 fields = "__all__" # noqa: DJ007 

210 

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

216 

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 ) 

241 

242 

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

260 

261 new_obj: Group 

262 

263 def action_checkbox(self, obj): 

264 if obj.internal: 

265 return "" 

266 return super().action_checkbox(obj) 

267 

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) 

272 

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) 

277 

278 def save_model(self, request: AuthenticatedHttpRequest, obj, form, change) -> None: 

279 """ 

280 Fix saving of automatic language/project selection, part 1. 

281 

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 

286 

287 def save_related( 

288 self, request: AuthenticatedHttpRequest, form, formsets, change 

289 ) -> None: 

290 """ 

291 Fix saving of automatic language/project selection, part 2. 

292 

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()