Fix admin API gaps for users, exchange checks, and parser logs

This commit is contained in:
2026-03-19 16:48:38 +01:00
parent 25176f31b4
commit 941c268d32
22 changed files with 817 additions and 28 deletions

View File

@@ -4,6 +4,7 @@ from apps.core.exceptions import NotFoundError
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.db import transaction
from django.db.models import F, Q
from rest_framework_simplejwt.tokens import RefreshToken
from .models import Profile
@@ -69,6 +70,65 @@ class UserService:
.order_by("-created_at")
)
@classmethod
def get_filtered_users_queryset(
cls,
*,
search: str = "",
ordering: str = "",
):
"""Queryset списка пользователей с поиском и сортировкой для админского UI."""
queryset = cls.get_users_queryset()
search_term = search.strip()
if search_term:
queryset = queryset.filter(
Q(username__icontains=search_term)
| Q(email__icontains=search_term)
| Q(phone__icontains=search_term)
| Q(profile__first_name__icontains=search_term)
| Q(profile__middle_name__icontains=search_term)
| Q(profile__last_name__icontains=search_term)
).distinct()
ordering_fields = []
ordering_map = {
"id": ("id", False),
"email": ("email", False),
"username": ("username", False),
"phone": ("phone", False),
"is_active": ("is_active", False),
"is_verified": ("is_verified", False),
"created_at": ("created_at", False),
"updated_at": ("updated_at", False),
"first_name": ("profile__first_name", True),
"middle_name": ("profile__middle_name", True),
"last_name": ("profile__last_name", True),
"role": ("is_staff", False),
}
for raw_field in (item.strip() for item in ordering.split(",") if item.strip()):
is_desc = raw_field.startswith("-")
field_name = raw_field[1:] if is_desc else raw_field
mapped_config = ordering_map.get(field_name)
if not mapped_config:
continue
mapped_field, nulls_last = mapped_config
if nulls_last:
ordering_fields.append(
F(mapped_field).desc(nulls_last=True)
if is_desc
else F(mapped_field).asc(nulls_last=True)
)
continue
ordering_fields.append(f"-{mapped_field}" if is_desc else mapped_field)
if ordering_fields:
queryset = queryset.order_by(*ordering_fields, "-created_at")
return queryset
@classmethod
def get_user_by_email(cls, email: str) -> User:
"""Получает пользователя по email
@@ -147,8 +207,9 @@ class UserService:
username: str,
password: str,
role: str,
first_name: str | None = None,
last_name: str | None = None,
first_name: str,
last_name: str,
middle_name: str | None = None,
**extra_fields,
) -> User:
"""Создаёт пользователя администратором и назначает роль."""
@@ -162,6 +223,7 @@ class UserService:
cls._update_or_create_profile(
user=user,
first_name=first_name,
middle_name=middle_name,
last_name=last_name,
)
return cls.get_users_queryset().get(id=user.id)
@@ -174,7 +236,9 @@ class UserService:
role = fields.pop("role", None)
password = fields.pop("password", None)
profile_fields = {
key: fields.pop(key) for key in ("first_name", "last_name") if key in fields
key: fields.pop(key)
for key in ("first_name", "middle_name", "last_name")
if key in fields
}
for field, value in fields.items():
@@ -201,6 +265,14 @@ class UserService:
user.save(update_fields=["is_active"])
return user
@classmethod
def activate_user(cls, user_id: int) -> User:
"""Активирует пользователя."""
user = cls.get_user_by_id(user_id)
user.is_active = True
user.save(update_fields=["is_active"])
return user
@classmethod
def delete_user(cls, user_id: int) -> None:
"""
@@ -327,11 +399,14 @@ class UserService:
*,
user: User,
first_name: str | None = None,
middle_name: str | None = None,
last_name: str | None = None,
) -> Profile:
profile, _ = Profile.objects.get_or_create(user=user)
if first_name is not None:
profile.first_name = first_name
if middle_name is not None:
profile.middle_name = middle_name
if last_name is not None:
profile.last_name = last_name
profile.save()
@@ -410,6 +485,7 @@ class ProfileService:
"is_verified": user.is_verified,
"phone": user.phone,
"first_name": profile.first_name,
"middle_name": profile.middle_name,
"last_name": profile.last_name,
"full_name": profile.full_name,
"bio": profile.bio,