Refactor project structure and update configurations for State Corp backend
- Updated project description in __init__.py - Enhanced .gitignore to exclude additional data files - Modified User model to remove first_name and last_name fields - Improved instance save method in services.py to include updated_at field - Added API tokens to .env.example for external services - Cleaned up test files for better readability - Updated Dockerfile and docker-compose.yml for improved setup - Revised README.md to reflect project changes and added changelog
This commit is contained in:
@@ -107,7 +107,10 @@ class BaseService(Generic[M]):
|
||||
"""
|
||||
for field, value in kwargs.items():
|
||||
setattr(instance, field, value)
|
||||
instance.save(update_fields=list(kwargs.keys()))
|
||||
update_fields = set(kwargs.keys())
|
||||
if hasattr(instance, "updated_at"):
|
||||
update_fields.add("updated_at")
|
||||
instance.save(update_fields=list(update_fields))
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -267,3 +267,101 @@ class PeriodicTask(TimedTask):
|
||||
"periodic": True,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class TrackedTask(TimedTask):
|
||||
"""
|
||||
Задача с отслеживанием в BackgroundJob.
|
||||
|
||||
Автоматически создаёт запись BackgroundJob при запуске
|
||||
и обновляет статус при завершении.
|
||||
|
||||
Пример использования:
|
||||
@app.task(base=TrackedTask, bind=True)
|
||||
def process_excel_file(self, file_path: str, user_id: int):
|
||||
job = self.get_job()
|
||||
job.update_progress(10, "Загрузка файла...")
|
||||
# ... обработка ...
|
||||
job.update_progress(50, "Обработка данных...")
|
||||
# ... обработка ...
|
||||
return {"loaded": 100}
|
||||
|
||||
# Запуск:
|
||||
task = process_excel_file.delay(file_path, user_id=request.user.id)
|
||||
# task.id содержит task_id для отслеживания
|
||||
"""
|
||||
|
||||
# Не делать авто-retry для отслеживаемых задач
|
||||
autoretry_for = ()
|
||||
max_retries = 0
|
||||
|
||||
def before_start(
|
||||
self,
|
||||
task_id: str,
|
||||
args: tuple[Any, ...],
|
||||
kwargs: dict[str, Any],
|
||||
) -> None:
|
||||
"""Создаёт запись BackgroundJob при старте."""
|
||||
super().before_start(task_id, args, kwargs)
|
||||
|
||||
from apps.core.services import BackgroundJobService
|
||||
|
||||
# Получаем user_id из kwargs если есть
|
||||
user_id = kwargs.get("user_id")
|
||||
|
||||
BackgroundJobService.create_job(
|
||||
task_id=task_id,
|
||||
task_name=self.name,
|
||||
user_id=user_id,
|
||||
meta={"args": str(args)[:500], "kwargs": str(kwargs)[:500]},
|
||||
)
|
||||
|
||||
# Отмечаем как запущенную
|
||||
job = BackgroundJobService.get_by_task_id(task_id)
|
||||
job.mark_started()
|
||||
|
||||
def on_success(
|
||||
self,
|
||||
retval: Any,
|
||||
task_id: str,
|
||||
args: tuple[Any, ...],
|
||||
kwargs: dict[str, Any],
|
||||
) -> None:
|
||||
"""Отмечает задачу как успешно завершённую."""
|
||||
super().on_success(retval, task_id, args, kwargs)
|
||||
|
||||
from apps.core.services import BackgroundJobService
|
||||
|
||||
job = BackgroundJobService.get_by_task_id_or_none(task_id)
|
||||
if job:
|
||||
job.complete(result=retval)
|
||||
|
||||
def on_failure(
|
||||
self,
|
||||
exc: Exception,
|
||||
task_id: str,
|
||||
args: tuple[Any, ...],
|
||||
kwargs: dict[str, Any],
|
||||
einfo: Any,
|
||||
) -> None:
|
||||
"""Отмечает задачу как завершённую с ошибкой."""
|
||||
super().on_failure(exc, task_id, args, kwargs, einfo)
|
||||
|
||||
from apps.core.services import BackgroundJobService
|
||||
|
||||
job = BackgroundJobService.get_by_task_id_or_none(task_id)
|
||||
if job:
|
||||
job.fail(
|
||||
error=str(exc),
|
||||
traceback_str=str(einfo) if einfo else "",
|
||||
)
|
||||
|
||||
def get_job(self):
|
||||
"""
|
||||
Получить объект BackgroundJob для текущей задачи.
|
||||
|
||||
Использовать внутри задачи для обновления прогресса.
|
||||
"""
|
||||
from apps.core.services import BackgroundJobService
|
||||
|
||||
return BackgroundJobService.get_by_task_id(self.request.id)
|
||||
|
||||
@@ -13,6 +13,7 @@ from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.request import Request
|
||||
@@ -21,29 +22,29 @@ from rest_framework.views import APIView
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Swagger теги
|
||||
HEALTH_TAG = "Мониторинг"
|
||||
JOBS_TAG = "Фоновые задачи"
|
||||
|
||||
|
||||
class HealthCheckView(APIView):
|
||||
"""
|
||||
Comprehensive health check endpoint.
|
||||
Комплексная проверка состояния системы.
|
||||
|
||||
GET /api/health/
|
||||
Returns detailed status of all dependencies.
|
||||
|
||||
Response:
|
||||
{
|
||||
"status": "healthy" | "degraded" | "unhealthy",
|
||||
"version": "1.0.0",
|
||||
"checks": {
|
||||
"database": {"status": "up", "latency_ms": 5},
|
||||
"redis": {"status": "up", "latency_ms": 2},
|
||||
"celery": {"status": "up"}
|
||||
}
|
||||
}
|
||||
Возвращает статус всех зависимостей (БД, Redis, Celery).
|
||||
"""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
authentication_classes = [] # No auth required
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=[HEALTH_TAG],
|
||||
operation_summary="Проверка состояния",
|
||||
operation_description=(
|
||||
"Комплексная проверка всех зависимостей системы.\n"
|
||||
"Возвращает статус: healthy, degraded или unhealthy."
|
||||
),
|
||||
)
|
||||
def get(self, request: Request) -> Response:
|
||||
"""Run all health checks and return status."""
|
||||
checks = {}
|
||||
@@ -131,15 +132,19 @@ class HealthCheckView(APIView):
|
||||
|
||||
class LivenessView(APIView):
|
||||
"""
|
||||
Kubernetes liveness probe endpoint.
|
||||
Kubernetes liveness probe.
|
||||
|
||||
GET /api/health/live/
|
||||
Returns 200 if the application is running.
|
||||
Проверяет, запущено ли приложение.
|
||||
"""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
authentication_classes = []
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=[HEALTH_TAG],
|
||||
operation_summary="Liveness probe",
|
||||
operation_description="Возвращает 200 если приложение запущено.",
|
||||
)
|
||||
def get(self, request: Request) -> Response:
|
||||
"""Simple liveness check."""
|
||||
return Response({"status": "alive"}, status=status.HTTP_200_OK)
|
||||
@@ -147,15 +152,21 @@ class LivenessView(APIView):
|
||||
|
||||
class ReadinessView(APIView):
|
||||
"""
|
||||
Kubernetes readiness probe endpoint.
|
||||
Kubernetes readiness probe.
|
||||
|
||||
GET /api/health/ready/
|
||||
Returns 200 if the application is ready to serve traffic.
|
||||
Проверяет, готово ли приложение обрабатывать запросы.
|
||||
"""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
authentication_classes = []
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=[HEALTH_TAG],
|
||||
operation_summary="Readiness probe",
|
||||
operation_description=(
|
||||
"Возвращает 200 если приложение готово обрабатывать запросы."
|
||||
),
|
||||
)
|
||||
def get(self, request: Request) -> Response:
|
||||
"""Check if app is ready to serve traffic."""
|
||||
# Check database connection
|
||||
@@ -177,26 +188,21 @@ class BackgroundJobStatusView(APIView):
|
||||
"""
|
||||
Получение статуса фоновой задачи.
|
||||
|
||||
GET /api/v1/jobs/{task_id}/
|
||||
Возвращает статус, прогресс и результат задачи.
|
||||
|
||||
Response:
|
||||
{
|
||||
"id": "uuid",
|
||||
"task_id": "celery-task-id",
|
||||
"status": "pending|started|success|failure|revoked",
|
||||
"progress": 75,
|
||||
"progress_message": "Обработка данных...",
|
||||
"result": {...},
|
||||
"error": "",
|
||||
"is_finished": false
|
||||
}
|
||||
"""
|
||||
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=[JOBS_TAG],
|
||||
operation_summary="Статус задачи",
|
||||
operation_description=(
|
||||
"Возвращает статус конкретной фоновой задачи.\n"
|
||||
"Доступно только владельцу задачи или администратору."
|
||||
),
|
||||
)
|
||||
def get(self, request: Request, task_id: str) -> Response:
|
||||
"""Получить статус задачи по task_id."""
|
||||
from apps.core.serializers import BackgroundJobSerializer
|
||||
@@ -219,18 +225,21 @@ class BackgroundJobListView(APIView):
|
||||
"""
|
||||
Список фоновых задач пользователя.
|
||||
|
||||
GET /api/v1/jobs/
|
||||
Возвращает список задач текущего пользователя.
|
||||
|
||||
Query params:
|
||||
status: Фильтр по статусу (pending, started, success, failure)
|
||||
limit: Количество записей (по умолчанию 50)
|
||||
Возвращает список задач текущего пользователя с фильтрацией.
|
||||
"""
|
||||
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=[JOBS_TAG],
|
||||
operation_summary="Список задач",
|
||||
operation_description=(
|
||||
"Возвращает список фоновых задач текущего пользователя.\n"
|
||||
"Поддерживает фильтрацию по статусу (status) и лимит (limit)."
|
||||
),
|
||||
)
|
||||
def get(self, request: Request) -> Response:
|
||||
"""Получить список задач пользователя."""
|
||||
from apps.core.serializers import BackgroundJobListSerializer
|
||||
|
||||
@@ -6,6 +6,10 @@ from django.utils.translation import gettext_lazy as _
|
||||
class User(AbstractUser):
|
||||
"""Расширенная модель пользователя"""
|
||||
|
||||
# Убираем first_name и last_name из модели User (они в Profile)
|
||||
first_name = None
|
||||
last_name = None
|
||||
|
||||
# Переопределяем группы и разрешения для избежания конфликта
|
||||
groups = models.ManyToManyField(
|
||||
"auth.Group",
|
||||
|
||||
@@ -8,7 +8,6 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
from .serializers import (
|
||||
LoginSerializer,
|
||||
PasswordChangeSerializer,
|
||||
@@ -20,14 +19,26 @@ from .serializers import (
|
||||
)
|
||||
from .services import ProfileService, UserService
|
||||
|
||||
# Swagger теги для группировки
|
||||
AUTH_TAG = "Аутентификация"
|
||||
USER_TAG = "Пользователь"
|
||||
|
||||
|
||||
class RegisterView(APIView):
|
||||
"""Регистрация нового пользователя"""
|
||||
"""
|
||||
Регистрация нового пользователя.
|
||||
|
||||
Создаёт учётную запись и возвращает JWT токены.
|
||||
"""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@swagger_auto_schema(
|
||||
request_body=UserRegistrationSerializer, responses={201: UserSerializer}
|
||||
tags=[AUTH_TAG],
|
||||
operation_summary="Регистрация",
|
||||
operation_description="Создание новой учётной записи пользователя.",
|
||||
request_body=UserRegistrationSerializer,
|
||||
responses={201: UserSerializer},
|
||||
)
|
||||
def post(self, request):
|
||||
serializer = UserRegistrationSerializer(data=request.data)
|
||||
@@ -49,11 +60,21 @@ class RegisterView(APIView):
|
||||
|
||||
|
||||
class LoginView(APIView):
|
||||
"""Вход пользователя"""
|
||||
"""
|
||||
Вход пользователя.
|
||||
|
||||
Возвращает access и refresh токены для авторизации.
|
||||
"""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@swagger_auto_schema(request_body=LoginSerializer, responses={200: TokenSerializer})
|
||||
@swagger_auto_schema(
|
||||
tags=[AUTH_TAG],
|
||||
operation_summary="Вход",
|
||||
operation_description="Аутентификация по email и паролю. Возвращает JWT токены.",
|
||||
request_body=LoginSerializer,
|
||||
responses={200: TokenSerializer},
|
||||
)
|
||||
def post(self, request):
|
||||
serializer = LoginSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
@@ -74,50 +95,35 @@ class LoginView(APIView):
|
||||
|
||||
|
||||
class LogoutView(APIView):
|
||||
"""Выход пользователя"""
|
||||
"""
|
||||
Выход пользователя.
|
||||
|
||||
Логаут на JWT означает удаление токенов на клиенте.
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
"Authorization",
|
||||
openapi.IN_HEADER,
|
||||
description="Bearer <token>",
|
||||
type=openapi.TYPE_STRING,
|
||||
required=True,
|
||||
)
|
||||
],
|
||||
tags=[AUTH_TAG],
|
||||
operation_summary="Выход",
|
||||
operation_description="Выход из системы (удаление токенов на клиенте).",
|
||||
responses={200: "Успешный выход"},
|
||||
)
|
||||
def post(self, request):
|
||||
try:
|
||||
refresh_token = request.data.get("refresh")
|
||||
if refresh_token:
|
||||
token = RefreshToken(refresh_token)
|
||||
token.blacklist()
|
||||
return Response({"message": "Успешный выход"}, status=status.HTTP_200_OK)
|
||||
except Exception:
|
||||
return Response(
|
||||
{"error": "Неверный токен"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
# Для JWT логаут означает удаление токенов на клиенте.
|
||||
# Сервер не хранит сессию и ничего не инвалидирует.
|
||||
return Response({"message": "Успешный выход"}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class CurrentUserView(APIView):
|
||||
"""Получение данных текущего пользователя"""
|
||||
"""Получение данных текущего пользователя."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
"Authorization",
|
||||
openapi.IN_HEADER,
|
||||
description="Bearer <token>",
|
||||
type=openapi.TYPE_STRING,
|
||||
required=True,
|
||||
)
|
||||
],
|
||||
tags=[USER_TAG],
|
||||
operation_summary="Текущий пользователь",
|
||||
operation_description="Возвращает данные авторизованного пользователя.",
|
||||
responses={200: UserSerializer},
|
||||
)
|
||||
def get(self, request):
|
||||
@@ -126,21 +132,15 @@ class CurrentUserView(APIView):
|
||||
|
||||
|
||||
class UserUpdateView(APIView):
|
||||
"""Обновление данных пользователя"""
|
||||
"""Обновление данных пользователя."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=[USER_TAG],
|
||||
operation_summary="Обновить данные",
|
||||
operation_description="Частичное обновление данных пользователя.",
|
||||
request_body=UserUpdateSerializer,
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
"Authorization",
|
||||
openapi.IN_HEADER,
|
||||
description="Bearer <token>",
|
||||
type=openapi.TYPE_STRING,
|
||||
required=True,
|
||||
)
|
||||
],
|
||||
responses={200: UserSerializer},
|
||||
)
|
||||
def patch(self, request):
|
||||
@@ -153,7 +153,7 @@ class UserUpdateView(APIView):
|
||||
|
||||
|
||||
class ProfileDetailView(generics.RetrieveUpdateAPIView):
|
||||
"""Получение и обновление профиля пользователя"""
|
||||
"""Получение и обновление профиля пользователя."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = ProfileUpdateSerializer
|
||||
@@ -168,15 +168,9 @@ class ProfileDetailView(generics.RetrieveUpdateAPIView):
|
||||
return profile
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
"Authorization",
|
||||
openapi.IN_HEADER,
|
||||
description="Bearer <token>",
|
||||
type=openapi.TYPE_STRING,
|
||||
required=True,
|
||||
)
|
||||
]
|
||||
tags=[USER_TAG],
|
||||
operation_summary="Получить профиль",
|
||||
operation_description="Возвращает профиль текущего пользователя.",
|
||||
)
|
||||
def get(self, request, *args, **kwargs):
|
||||
profile = self.get_object()
|
||||
@@ -184,16 +178,10 @@ class ProfileDetailView(generics.RetrieveUpdateAPIView):
|
||||
return Response(serializer.data)
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=[USER_TAG],
|
||||
operation_summary="Обновить профиль",
|
||||
operation_description="Частичное обновление профиля пользователя.",
|
||||
request_body=ProfileUpdateSerializer,
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
"Authorization",
|
||||
openapi.IN_HEADER,
|
||||
description="Bearer <token>",
|
||||
type=openapi.TYPE_STRING,
|
||||
required=True,
|
||||
)
|
||||
],
|
||||
)
|
||||
def patch(self, request, *args, **kwargs):
|
||||
profile = self.get_object()
|
||||
@@ -207,21 +195,15 @@ class ProfileDetailView(generics.RetrieveUpdateAPIView):
|
||||
|
||||
|
||||
class PasswordChangeView(APIView):
|
||||
"""Смена пароля"""
|
||||
"""Смена пароля пользователя."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=[USER_TAG],
|
||||
operation_summary="Сменить пароль",
|
||||
operation_description="Смена пароля. Требуется текущий пароль для подтверждения.",
|
||||
request_body=PasswordChangeSerializer,
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
"Authorization",
|
||||
openapi.IN_HEADER,
|
||||
description="Bearer <token>",
|
||||
type=openapi.TYPE_STRING,
|
||||
required=True,
|
||||
)
|
||||
],
|
||||
responses={200: "Пароль успешно изменен"},
|
||||
)
|
||||
def post(self, request):
|
||||
@@ -246,20 +228,29 @@ class PasswordChangeView(APIView):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@swagger_auto_schema(
|
||||
method="get",
|
||||
tags=[USER_TAG],
|
||||
operation_summary="Полный профиль",
|
||||
operation_description="Расширенная информация о пользователе и профиле.",
|
||||
)
|
||||
@api_view(["GET"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def user_profile_detail(request):
|
||||
"""Получение полных данных профиля пользователя"""
|
||||
"""Получение полных данных профиля пользователя."""
|
||||
profile_data = ProfileService.get_full_profile_data(request.user.id)
|
||||
return Response(profile_data)
|
||||
|
||||
|
||||
class TokenRefreshView(APIView):
|
||||
"""Обновление access токена через refresh токен"""
|
||||
"""Обновление access токена через refresh токен."""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=[AUTH_TAG],
|
||||
operation_summary="Обновить токен",
|
||||
operation_description="Получение нового access токена по refresh токену.",
|
||||
request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
|
||||
Reference in New Issue
Block a user