feat: expand platform APIs, sources, and test coverage
Some checks failed
CI/CD Pipeline / Run Tests (pull_request) Successful in 1m53s
CI/CD Pipeline / Telegram Notify Success (push) Has been cancelled
CI/CD Pipeline / Run Tests (push) Has been cancelled
CI/CD Pipeline / Code Quality Checks (push) Has been cancelled
CI/CD Pipeline / Code Quality Checks (pull_request) Failing after 2m54s
CI/CD Pipeline / Telegram Notify Success (pull_request) Has been skipped
Some checks failed
CI/CD Pipeline / Run Tests (pull_request) Successful in 1m53s
CI/CD Pipeline / Telegram Notify Success (push) Has been cancelled
CI/CD Pipeline / Run Tests (push) Has been cancelled
CI/CD Pipeline / Code Quality Checks (push) Has been cancelled
CI/CD Pipeline / Code Quality Checks (pull_request) Failing after 2m54s
CI/CD Pipeline / Telegram Notify Success (pull_request) Has been skipped
This commit is contained in:
@@ -2,6 +2,7 @@ from typing import Any
|
||||
|
||||
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 rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
@@ -13,9 +14,27 @@ User = get_user_model()
|
||||
class UserService:
|
||||
"""Сервисный слой для работы с пользователями"""
|
||||
|
||||
ROLE_USER = "user"
|
||||
ROLE_ADMIN = "admin"
|
||||
ROLE_CHOICES = (
|
||||
(ROLE_USER, "Пользователь"),
|
||||
(ROLE_ADMIN, "Администратор"),
|
||||
)
|
||||
ROLE_LABELS = dict(ROLE_CHOICES)
|
||||
SETTINGS_SECTION_EXCHANGE = "exchange"
|
||||
SETTINGS_SECTION_PARSERS = "parsers"
|
||||
SETTINGS_SECTION_DATABASE_CONNECTION = "database_connection"
|
||||
SETTINGS_SECTION_REGISTERS_UPLOAD = "registers_upload"
|
||||
|
||||
@classmethod
|
||||
def create_user(
|
||||
cls, *, email: str, username: str, password: str, **extra_fields
|
||||
cls,
|
||||
*,
|
||||
email: str,
|
||||
username: str,
|
||||
password: str,
|
||||
role: str | None = None,
|
||||
**extra_fields,
|
||||
) -> User:
|
||||
"""
|
||||
Создает нового пользователя
|
||||
@@ -32,12 +51,24 @@ class UserService:
|
||||
Raises:
|
||||
ValidationError: При некорректных данных
|
||||
"""
|
||||
role = role or cls.ROLE_USER
|
||||
with transaction.atomic():
|
||||
user = User.objects.create_user(
|
||||
email=email, username=username, password=password, **extra_fields
|
||||
)
|
||||
cls.assign_role(user, role)
|
||||
return user
|
||||
|
||||
@classmethod
|
||||
def get_users_queryset(cls):
|
||||
"""Базовый queryset для админского списка пользователей."""
|
||||
return (
|
||||
User.objects.all()
|
||||
.select_related("profile")
|
||||
.prefetch_related("groups")
|
||||
.order_by("-created_at")
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_user_by_email(cls, email: str) -> User:
|
||||
"""Получает пользователя по email
|
||||
@@ -107,6 +138,69 @@ class UserService:
|
||||
user.save()
|
||||
return user
|
||||
|
||||
@classmethod
|
||||
@transaction.atomic
|
||||
def create_managed_user(
|
||||
cls,
|
||||
*,
|
||||
email: str,
|
||||
username: str,
|
||||
password: str,
|
||||
role: str,
|
||||
first_name: str | None = None,
|
||||
last_name: str | None = None,
|
||||
**extra_fields,
|
||||
) -> User:
|
||||
"""Создаёт пользователя администратором и назначает роль."""
|
||||
user = cls.create_user(
|
||||
email=email,
|
||||
username=username,
|
||||
password=password,
|
||||
role=role,
|
||||
**extra_fields,
|
||||
)
|
||||
cls._update_or_create_profile(
|
||||
user=user,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
)
|
||||
return cls.get_users_queryset().get(id=user.id)
|
||||
|
||||
@classmethod
|
||||
@transaction.atomic
|
||||
def update_managed_user(cls, user_id: int, **fields) -> User:
|
||||
"""Обновляет пользователя и его роль из админского интерфейса."""
|
||||
user = cls.get_user_by_id(user_id)
|
||||
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
|
||||
}
|
||||
|
||||
for field, value in fields.items():
|
||||
setattr(user, field, value)
|
||||
|
||||
if password:
|
||||
user.set_password(password)
|
||||
|
||||
user.save()
|
||||
|
||||
if role is not None:
|
||||
cls.assign_role(user, role)
|
||||
|
||||
if profile_fields:
|
||||
cls._update_or_create_profile(user=user, **profile_fields)
|
||||
|
||||
return cls.get_users_queryset().get(id=user.id)
|
||||
|
||||
@classmethod
|
||||
def deactivate_user(cls, user_id: int) -> User:
|
||||
"""Деактивирует пользователя без физического удаления."""
|
||||
user = cls.get_user_by_id(user_id)
|
||||
user.is_active = False
|
||||
user.save(update_fields=["is_active"])
|
||||
return user
|
||||
|
||||
@classmethod
|
||||
def delete_user(cls, user_id: int) -> None:
|
||||
"""
|
||||
@@ -138,6 +232,76 @@ class UserService:
|
||||
"access": str(refresh.access_token),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def ensure_role_groups(cls) -> dict[str, Group]:
|
||||
"""Гарантирует существование системных role-групп."""
|
||||
groups: dict[str, Group] = {}
|
||||
for role, _label in cls.ROLE_CHOICES:
|
||||
group, _ = Group.objects.get_or_create(name=role)
|
||||
groups[role] = group
|
||||
return groups
|
||||
|
||||
@classmethod
|
||||
def get_user_role(cls, user: User) -> str:
|
||||
"""Возвращает прикладную роль пользователя."""
|
||||
if user.is_superuser or user.is_staff:
|
||||
return cls.ROLE_ADMIN
|
||||
|
||||
group_names = {group.name for group in user.groups.all()}
|
||||
if cls.ROLE_ADMIN in group_names:
|
||||
return cls.ROLE_ADMIN
|
||||
return cls.ROLE_USER
|
||||
|
||||
@classmethod
|
||||
def get_role_label(cls, role: str) -> str:
|
||||
"""Возвращает человекочитаемое название роли."""
|
||||
return cls.ROLE_LABELS.get(role, role)
|
||||
|
||||
@classmethod
|
||||
def get_user_capabilities(cls, user: User) -> dict[str, Any]:
|
||||
"""Возвращает фронтовые capability flags по роли пользователя."""
|
||||
is_admin = cls.get_user_role(user) == cls.ROLE_ADMIN
|
||||
return {
|
||||
"can_manage_users": is_admin,
|
||||
"can_manage_exchange": is_admin,
|
||||
"can_manage_parsers": is_admin,
|
||||
"can_manage_database_connection": is_admin,
|
||||
"can_upload_registers": is_admin,
|
||||
"can_refresh_dashboard": is_admin,
|
||||
"settings_sections": (
|
||||
[
|
||||
cls.SETTINGS_SECTION_EXCHANGE,
|
||||
cls.SETTINGS_SECTION_PARSERS,
|
||||
cls.SETTINGS_SECTION_DATABASE_CONNECTION,
|
||||
cls.SETTINGS_SECTION_REGISTERS_UPLOAD,
|
||||
]
|
||||
if is_admin
|
||||
else []
|
||||
),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def assign_role(cls, user: User, role: str) -> User:
|
||||
"""Назначает одну из системных ролей через auth.Group и is_staff."""
|
||||
if role not in cls.ROLE_LABELS:
|
||||
raise ValueError(f"Unsupported role: {role}")
|
||||
|
||||
groups = cls.ensure_role_groups()
|
||||
role_group_names = list(groups.keys())
|
||||
current_role_groups = list(user.groups.filter(name__in=role_group_names))
|
||||
if current_role_groups:
|
||||
user.groups.remove(*current_role_groups)
|
||||
user.groups.add(groups[role])
|
||||
|
||||
if role == cls.ROLE_ADMIN:
|
||||
user.is_staff = True
|
||||
else:
|
||||
user.is_staff = False
|
||||
user.is_superuser = False
|
||||
|
||||
user.save()
|
||||
return user
|
||||
|
||||
@classmethod
|
||||
def verify_email(cls, user_id: int) -> User:
|
||||
"""
|
||||
@@ -157,6 +321,22 @@ class UserService:
|
||||
user.save()
|
||||
return user
|
||||
|
||||
@classmethod
|
||||
def _update_or_create_profile(
|
||||
cls,
|
||||
*,
|
||||
user: User,
|
||||
first_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 last_name is not None:
|
||||
profile.last_name = last_name
|
||||
profile.save()
|
||||
return profile
|
||||
|
||||
|
||||
class ProfileService:
|
||||
"""Сервисный слой для работы с профилями"""
|
||||
|
||||
Reference in New Issue
Block a user