Files
state-corp-backend/src/apps/user/admin.py
Aleksandr Meshchryakov e99616ed6d
All checks were successful
State Corp Backend CI/CD / Quality gate (push) Successful in 2m42s
State Corp Backend CI/CD / Build linux/amd64 images once (push) Successful in 5m29s
State Corp Backend CI/CD / Refresh and release internal main (push) Has been skipped
State Corp Backend CI/CD / Release customer main (push) Has been skipped
State Corp Backend CI/CD / Release dev (push) Successful in 36s
feat(users): add transactional employee deletion and token revocation
2026-09-13 22:55:31 +02:00

255 lines
8.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Admin configuration for user app."""
from contextlib import suppress
from functools import wraps
from apps.core.exceptions import BaseAPIException
from apps.core.models import BackgroundJob
from apps.user.management import lock_managed_users, validate_access_change
from apps.user.models import Profile, User
from apps.user.services import UserService
from django.contrib import admin, messages
from django.contrib.admin.sites import NotRegistered
from django.contrib.admin.utils import unquote
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import Group
from django.db import transaction
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
try:
from django_celery_beat.models import (
ClockedSchedule,
CrontabSchedule,
IntervalSchedule,
PeriodicTask,
SolarSchedule,
)
except ImportError: # pragma: no cover - optional dependency import guard
ClockedSchedule = None
CrontabSchedule = None
IntervalSchedule = None
PeriodicTask = None
SolarSchedule = None
try:
from rest_framework_simplejwt.token_blacklist.models import (
BlacklistedToken,
OutstandingToken,
)
except ImportError: # pragma: no cover - optional dependency import guard
BlacklistedToken = None
OutstandingToken = None
def _unregister(model) -> None:
if model is None:
return
with suppress(NotRegistered):
admin.site.unregister(model)
_unregister(User)
def _management_errors(view):
"""Present guarded account-change failures through Django admin messages."""
@wraps(view)
def wrapped(self, request, *args, **kwargs):
try:
return view(self, request, *args, **kwargs)
except BaseAPIException as exc:
self.message_user(request, exc.message, level=messages.ERROR)
return HttpResponseRedirect(reverse("admin:user_user_changelist"))
return wrapped
class ProfileInline(admin.StackedInline):
"""Inline для профиля пользователя."""
model = Profile
extra = 0
max_num = 1
can_delete = False
verbose_name_plural = "Профиль"
fk_name = "user"
fields = ["first_name", "mid_name", "last_name", "bio", "avatar", "date_of_birth"]
@admin.register(User)
class UserAdmin(BaseUserAdmin):
"""Admin для пользователей без групп."""
inlines = [ProfileInline]
list_display = [
"username",
"email",
"phone",
"is_verified_badge",
"is_active_badge",
"is_staff",
"created_at",
]
list_filter = ["is_staff", "is_superuser", "is_active", "is_verified", "created_at"]
search_fields = ["username", "email", "phone"]
ordering = ["-created_at"]
list_per_page = 50
date_hierarchy = "created_at"
fieldsets = (
(None, {"fields": ("username", "password")}),
(_("Personal info"), {"fields": ("email", "phone")}),
(
_("Permissions"),
{
"fields": (
"is_active",
"is_staff",
"is_superuser",
"is_verified",
"user_permissions",
),
"classes": ("collapse",),
},
),
(
_("Important dates"),
{"fields": ("last_login", "date_joined", "created_at", "updated_at")},
),
)
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": (
"username",
"email",
"password1",
"password2",
"is_staff",
"is_active",
),
},
),
)
readonly_fields = ["created_at", "updated_at", "last_login", "date_joined"]
actions = ["verify_users", "unverify_users", "activate_users", "deactivate_users"]
@_management_errors
def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
return super().changeform_view(request, object_id, form_url, extra_context)
@_management_errors
def delete_view(self, request, object_id, extra_context=None):
return super().delete_view(request, object_id, extra_context)
@_management_errors
def user_change_password(self, request, id, form_url=""):
user = self.get_object(request, unquote(id))
if request.method == "POST" and user is not None:
with lock_managed_users(actor_id=request.user.pk, user_ids=[user.pk]):
return super().user_change_password(request, id, form_url)
return super().user_change_password(request, id, form_url)
@_management_errors
def changelist_view(self, request, extra_context=None):
# delete_selected writes LogEntry rows before delete_queryset. Roll those
# back too if the complete selection fails an account-management guard.
with transaction.atomic():
return super().changelist_view(request, extra_context)
def save_model(self, request, obj, form, change):
with lock_managed_users(
actor_id=request.user.pk, user_ids=[obj.pk] if change else []
) as users:
if change:
validate_access_change(
users[obj.pk],
actor_id=request.user.pk,
is_active=obj.is_active,
is_staff=obj.is_staff,
)
super().save_model(request, obj, form, change)
def delete_model(self, request, obj):
UserService.delete_user(obj.pk, actor_id=request.user.pk)
def delete_queryset(self, request, queryset):
UserService.delete_users(
queryset.values_list("pk", flat=True), actor_id=request.user.pk
)
def is_verified_badge(self, obj):
if obj.is_verified:
return format_html(
'<span style="color: white; background: #28a745; padding: 3px 10px; border-radius: 3px;">✓</span>'
)
return format_html(
'<span style="color: white; background: #dc3545; padding: 3px 10px; border-radius: 3px;">✗</span>'
)
is_verified_badge.short_description = "Верифицирован"
is_verified_badge.admin_order_field = "is_verified"
def is_active_badge(self, obj):
if obj.is_active:
return format_html(
'<span style="color: white; background: #28a745; padding: 3px 10px; border-radius: 3px;">Активен</span>'
)
return format_html(
'<span style="color: white; background: #dc3545; padding: 3px 10px; border-radius: 3px;">Неактивен</span>'
)
is_active_badge.short_description = "Статус"
is_active_badge.admin_order_field = "is_active"
@admin.action(description="Верифицировать выбранных пользователей")
def verify_users(self, request, queryset):
updated = queryset.update(is_verified=True)
self.message_user(request, f"Верифицировано {updated} пользователей")
@admin.action(description="Снять верификацию")
def unverify_users(self, request, queryset):
updated = queryset.update(is_verified=False)
self.message_user(request, f"Снята верификация у {updated} пользователей")
@admin.action(description="Активировать пользователей")
def activate_users(self, request, queryset):
updated = UserService.set_users_active(
queryset.values_list("pk", flat=True),
actor_id=request.user.pk,
is_active=True,
)
self.message_user(request, f"Активировано {updated} пользователей")
@admin.action(description="Деактивировать пользователей")
def deactivate_users(self, request, queryset):
updated = UserService.set_users_active(
queryset.values_list("pk", flat=True),
actor_id=request.user.pk,
is_active=False,
)
self.message_user(request, f"Деактивировано {updated} пользователей")
for model in (
Group,
BackgroundJob,
OutstandingToken,
BlacklistedToken,
PeriodicTask,
CrontabSchedule,
IntervalSchedule,
SolarSchedule,
ClockedSchedule,
):
_unregister(model)