feat(core): add core module with mixins, services, and background jobs

- Add Model Mixins: TimestampMixin, SoftDeleteMixin, AuditMixin, etc.
- Add Base Services: BaseService, BulkOperationsMixin, QueryOptimizerMixin
- Add Base ViewSets with bulk operations
- Add BackgroundJob model for Celery task tracking
- Add BaseAppCommand for management commands
- Add permissions, pagination, filters, cache, logging
- Migrate tests to factory_boy + faker
- Add CHANGELOG.md
- 297 tests passing
This commit is contained in:
2026-01-21 11:47:26 +01:00
parent 06b30fca02
commit f121445313
72 changed files with 9258 additions and 594 deletions

View File

@@ -1,5 +1,6 @@
from typing import Any, Dict, Optional
from typing import Any
from apps.core.exceptions import NotFoundError
from django.contrib.auth import get_user_model
from django.db import transaction
from rest_framework_simplejwt.tokens import RefreshToken
@@ -38,23 +39,53 @@ class UserService:
return user
@classmethod
def get_user_by_email(cls, email: str) -> Optional[User]:
"""Получает пользователя по email"""
def get_user_by_email(cls, email: str) -> User:
"""Получает пользователя по email
Raises:
NotFoundError: Если пользователь не найден
"""
try:
return User.objects.get(email=email)
except User.DoesNotExist as e:
raise NotFoundError(
message=f"User with email={email} not found",
details={"email": email},
) from e
@classmethod
def get_user_by_email_or_none(cls, email: str) -> User | None:
"""Получает пользователя по email или None"""
try:
return User.objects.get(email=email)
except User.DoesNotExist:
return None
@classmethod
def get_user_by_id(cls, user_id: int) -> Optional[User]:
"""Получает пользователя по ID"""
def get_user_by_id(cls, user_id: int) -> User:
"""Получает пользователя по ID
Raises:
NotFoundError: Если пользователь не найден
"""
try:
return User.objects.get(id=user_id)
except User.DoesNotExist as e:
raise NotFoundError(
message=f"User with id={user_id} not found",
details={"user_id": user_id},
) from e
@classmethod
def get_user_by_id_or_none(cls, user_id: int) -> User | None:
"""Получает пользователя по ID или None"""
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return None
@classmethod
def update_user(cls, user_id: int, **fields) -> Optional[User]:
def update_user(cls, user_id: int, **fields) -> User:
"""
Обновляет данные пользователя
@@ -63,11 +94,12 @@ class UserService:
**fields: Поля для обновления
Returns:
User: Обновленный пользователь или None
User: Обновленный пользователь
Raises:
NotFoundError: Если пользователь не найден
"""
user = cls.get_user_by_id(user_id)
if not user:
return None
for field, value in fields.items():
setattr(user, field, value)
@@ -76,24 +108,21 @@ class UserService:
return user
@classmethod
def delete_user(cls, user_id: int) -> bool:
def delete_user(cls, user_id: int) -> None:
"""
Удаляет пользователя
Args:
user_id: ID пользователя
Returns:
bool: True если успешно удален
Raises:
NotFoundError: Если пользователь не найден
"""
user = cls.get_user_by_id(user_id)
if user:
user.delete()
return True
return False
user.delete()
@classmethod
def get_tokens_for_user(cls, user: User) -> Dict[str, str]:
def get_tokens_for_user(cls, user: User) -> dict[str, str]:
"""
Генерирует JWT токены для пользователя
@@ -110,7 +139,7 @@ class UserService:
}
@classmethod
def verify_email(cls, user_id: int) -> bool:
def verify_email(cls, user_id: int) -> User:
"""
Подтверждает email пользователя
@@ -118,29 +147,45 @@ class UserService:
user_id: ID пользователя
Returns:
bool: True если успешно подтвержден
User: Обновленный пользователь
Raises:
NotFoundError: Если пользователь не найден
"""
user = cls.get_user_by_id(user_id)
if user:
user.is_verified = True
user.save()
return True
return False
user.is_verified = True
user.save()
return user
class ProfileService:
"""Сервисный слой для работы с профилями"""
@classmethod
def get_profile_by_user_id(cls, user_id: int) -> Optional[Profile]:
"""Получает профиль по ID пользователя"""
def get_profile_by_user_id(cls, user_id: int) -> Profile:
"""Получает профиль по ID пользователя
Raises:
NotFoundError: Если профиль не найден
"""
try:
return Profile.objects.select_related("user").get(user_id=user_id)
except Profile.DoesNotExist as e:
raise NotFoundError(
message=f"Profile for user_id={user_id} not found",
details={"user_id": user_id},
) from e
@classmethod
def get_profile_by_user_id_or_none(cls, user_id: int) -> Profile | None:
"""Получает профиль по ID пользователя или None"""
try:
return Profile.objects.select_related("user").get(user_id=user_id)
except Profile.DoesNotExist:
return None
@classmethod
def update_profile(cls, user_id: int, **fields) -> Optional[Profile]:
def update_profile(cls, user_id: int, **fields) -> Profile:
"""
Обновляет профиль пользователя
@@ -149,11 +194,12 @@ class ProfileService:
**fields: Поля для обновления
Returns:
Profile: Обновленный профиль или None
Profile: Обновленный профиль
Raises:
NotFoundError: Если профиль не найден
"""
profile = cls.get_profile_by_user_id(user_id)
if not profile:
return None
for field, value in fields.items():
setattr(profile, field, value)
@@ -162,7 +208,7 @@ class ProfileService:
return profile
@classmethod
def get_full_profile_data(cls, user_id: int) -> Optional[Dict[str, Any]]:
def get_full_profile_data(cls, user_id: int) -> dict[str, Any]:
"""
Получает полные данные пользователя и профиля
@@ -170,12 +216,12 @@ class ProfileService:
user_id: ID пользователя
Returns:
Dict: Полные данные или None
Dict: Полные данные
Raises:
NotFoundError: Если профиль не найден
"""
profile = cls.get_profile_by_user_id(user_id)
if not profile:
return None
user = profile.user
return {
"id": user.id,