848 lines
35 KiB
Python
848 lines
35 KiB
Python
"""
|
||
API views для пользователей и аутентификации.
|
||
"""
|
||
from rest_framework import generics, status, viewsets
|
||
from rest_framework.decorators import action
|
||
from rest_framework.response import Response
|
||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||
from rest_framework_simplejwt.tokens import RefreshToken
|
||
from django.utils import timezone
|
||
from django.db.models import Q
|
||
import secrets
|
||
import string
|
||
import random
|
||
import logging
|
||
|
||
from .models import User, Client, Parent, Group
|
||
from .serializers import (
|
||
UserDetailSerializer,
|
||
UserSerializer,
|
||
RegisterSerializer,
|
||
LoginSerializer,
|
||
TelegramAuthSerializer,
|
||
ChangePasswordSerializer,
|
||
PasswordResetRequestSerializer,
|
||
PasswordResetConfirmSerializer,
|
||
EmailVerificationSerializer,
|
||
ClientSerializer,
|
||
ParentSerializer,
|
||
GroupSerializer,
|
||
)
|
||
from .tasks import (
|
||
send_verification_email_task,
|
||
send_password_reset_email_task,
|
||
send_welcome_email_task,
|
||
)
|
||
from .permissions import IsMentor
|
||
from .telegram_auth import extract_telegram_user_data
|
||
from .telegram_utils import get_telegram_bot_username
|
||
from config.throttling import BurstRateThrottle
|
||
|
||
|
||
class TelegramBotInfoView(generics.GenericAPIView):
|
||
"""
|
||
API endpoint для получения информации о Telegram боте.
|
||
|
||
GET /api/auth/telegram/bot-info/
|
||
"""
|
||
permission_classes = [AllowAny]
|
||
authentication_classes = []
|
||
|
||
def get(self, request, *args, **kwargs):
|
||
"""Получение имени бота для использования в Telegram Login Widget."""
|
||
bot_username, error_message = get_telegram_bot_username()
|
||
|
||
if not bot_username:
|
||
return Response({
|
||
'success': False,
|
||
'error': error_message or 'Telegram бот не настроен или токен недействителен',
|
||
'bot_username': None,
|
||
'bot_url': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
return Response({
|
||
'success': True,
|
||
'bot_username': bot_username,
|
||
'bot_url': f'https://t.me/{bot_username}'
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
|
||
class TelegramAuthView(generics.GenericAPIView):
|
||
"""
|
||
API endpoint для авторизации через Telegram Login Widget.
|
||
|
||
POST /api/auth/telegram/
|
||
"""
|
||
serializer_class = TelegramAuthSerializer
|
||
permission_classes = [AllowAny]
|
||
authentication_classes = []
|
||
throttle_classes = [BurstRateThrottle]
|
||
|
||
def post(self, request, *args, **kwargs):
|
||
"""Авторизация или регистрация через Telegram."""
|
||
serializer = self.get_serializer(data=request.data)
|
||
serializer.is_valid(raise_exception=True)
|
||
|
||
telegram_data = serializer.validated_data
|
||
telegram_user_data = extract_telegram_user_data(telegram_data)
|
||
|
||
telegram_id = telegram_user_data['telegram_id']
|
||
role = telegram_data.get('role', 'client')
|
||
|
||
# Ищем пользователя по telegram_id
|
||
try:
|
||
user = User.objects.get(telegram_id=telegram_id)
|
||
# Обновляем данные пользователя
|
||
user.telegram_username = telegram_user_data.get('username', '') or user.telegram_username
|
||
if telegram_user_data.get('first_name'):
|
||
user.first_name = telegram_user_data['first_name']
|
||
if telegram_user_data.get('last_name'):
|
||
user.last_name = telegram_user_data['last_name']
|
||
user.last_activity = timezone.now()
|
||
user.save()
|
||
|
||
is_new_user = False
|
||
message = 'Вход выполнен успешно'
|
||
except User.DoesNotExist:
|
||
# Создаем нового пользователя
|
||
# Генерируем email на основе telegram_id, если не указан
|
||
email = f"telegram_{telegram_id}@telegram.local"
|
||
|
||
# Проверяем, не занят ли email
|
||
counter = 1
|
||
while User.objects.filter(email=email).exists():
|
||
email = f"telegram_{telegram_id}_{counter}@telegram.local"
|
||
counter += 1
|
||
|
||
user = User.objects.create_user(
|
||
email=email,
|
||
telegram_id=telegram_id,
|
||
telegram_username=telegram_user_data.get('username', ''),
|
||
first_name=telegram_user_data.get('first_name', ''),
|
||
last_name=telegram_user_data.get('last_name', ''),
|
||
role=role,
|
||
email_verified=True, # Telegram уже проверил пользователя
|
||
)
|
||
|
||
# Устанавливаем неприводимый пароль, чтобы пользователь не мог войти через email
|
||
user.set_unusable_password()
|
||
user.save()
|
||
|
||
is_new_user = True
|
||
message = 'Регистрация через Telegram выполнена успешно'
|
||
|
||
# Генерируем JWT токены
|
||
refresh = RefreshToken.for_user(user)
|
||
|
||
return Response({
|
||
'success': True,
|
||
'message': message,
|
||
'is_new_user': is_new_user,
|
||
'data': {
|
||
'user': UserDetailSerializer(user).data,
|
||
'tokens': {
|
||
'access': str(refresh.access_token),
|
||
'refresh': str(refresh),
|
||
}
|
||
}
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
|
||
class RegisterView(generics.CreateAPIView):
|
||
"""
|
||
API endpoint для регистрации нового пользователя.
|
||
|
||
POST /api/auth/register/
|
||
"""
|
||
queryset = User.objects.all()
|
||
serializer_class = RegisterSerializer
|
||
permission_classes = [AllowAny]
|
||
authentication_classes = []
|
||
throttle_classes = [BurstRateThrottle]
|
||
|
||
def create(self, request, *args, **kwargs):
|
||
"""Регистрация пользователя с отправкой email подтверждения."""
|
||
serializer = self.get_serializer(data=request.data)
|
||
serializer.is_valid(raise_exception=True)
|
||
user = serializer.save()
|
||
|
||
# Токен для подтверждения email
|
||
verification_token = secrets.token_urlsafe(32)
|
||
user.email_verification_token = verification_token
|
||
user.save(update_fields=['email_verification_token'])
|
||
|
||
# Отправляем email подтверждения (асинхронно через Celery)
|
||
send_verification_email_task.delay(user.id, verification_token)
|
||
|
||
# Генерируем JWT токены
|
||
refresh = RefreshToken.for_user(user)
|
||
|
||
# Берём пользователя из БД, чтобы в ответе точно был universal_code
|
||
user.refresh_from_db()
|
||
user_serializer = UserDetailSerializer(user, context={'request': request})
|
||
|
||
return Response({
|
||
'success': True,
|
||
'message': 'Регистрация успешна. Проверьте email для подтверждения аккаунта.',
|
||
'data': {
|
||
'user': user_serializer.data,
|
||
'tokens': {
|
||
'access': str(refresh.access_token),
|
||
'refresh': str(refresh),
|
||
}
|
||
}
|
||
}, status=status.HTTP_201_CREATED)
|
||
|
||
|
||
class LoginView(generics.GenericAPIView):
|
||
"""
|
||
API endpoint для входа пользователя.
|
||
|
||
POST /api/auth/login/
|
||
"""
|
||
serializer_class = LoginSerializer
|
||
permission_classes = [AllowAny]
|
||
authentication_classes = []
|
||
throttle_classes = [BurstRateThrottle]
|
||
|
||
def post(self, request, *args, **kwargs):
|
||
"""Вход пользователя с выдачей JWT токенов."""
|
||
serializer = self.get_serializer(data=request.data)
|
||
serializer.is_valid(raise_exception=True)
|
||
|
||
user = serializer.validated_data['user']
|
||
|
||
# Обновляем время последней активности
|
||
user.last_activity = timezone.now()
|
||
user.save(update_fields=['last_activity'])
|
||
|
||
# Генерируем JWT токены (выдаем даже если email не подтвержден)
|
||
refresh = RefreshToken.for_user(user)
|
||
|
||
# Формируем сообщение в зависимости от статуса email
|
||
if not user.email_verified:
|
||
message = 'Вход выполнен. Для завершения регистрации необходимо подтвердить email адрес.'
|
||
else:
|
||
message = 'Вход выполнен успешно'
|
||
|
||
# Сериализуем пользователя с контекстом запроса для правильных URL
|
||
user_serializer = UserDetailSerializer(user, context={'request': request})
|
||
|
||
return Response({
|
||
'success': True,
|
||
'message': message,
|
||
'data': {
|
||
'user': user_serializer.data,
|
||
'tokens': {
|
||
'access': str(refresh.access_token),
|
||
'refresh': str(refresh),
|
||
}
|
||
}
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
|
||
class LoginByTokenView(generics.GenericAPIView):
|
||
"""
|
||
API endpoint для входа по персональному токену.
|
||
|
||
POST /api/auth/login-by-token/
|
||
"""
|
||
permission_classes = [AllowAny]
|
||
authentication_classes = []
|
||
throttle_classes = [BurstRateThrottle]
|
||
|
||
def post(self, request, *args, **kwargs):
|
||
token = request.data.get('token')
|
||
if not token:
|
||
return Response({'error': 'Токен обязателен'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
user = User.objects.get(login_token=token)
|
||
if not user.is_active:
|
||
return Response({'error': 'Аккаунт неактивен'}, status=status.HTTP_403_FORBIDDEN)
|
||
if user.is_blocked:
|
||
return Response({'error': f'Аккаунт заблокирован. Причина: {user.blocked_reason}'}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
user.last_login = timezone.now()
|
||
user.last_activity = timezone.now()
|
||
user.save(update_fields=['last_login', 'last_activity'])
|
||
|
||
refresh = RefreshToken.for_user(user)
|
||
user_serializer = UserDetailSerializer(user, context={'request': request})
|
||
|
||
return Response({
|
||
'success': True,
|
||
'message': 'Вход выполнен успешно',
|
||
'data': {
|
||
'user': user_serializer.data,
|
||
'tokens': {
|
||
'access': str(refresh.access_token),
|
||
'refresh': str(refresh),
|
||
}
|
||
}
|
||
}, status=status.HTTP_200_OK)
|
||
except User.DoesNotExist:
|
||
return Response({'error': 'Неверный токен'}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
|
||
class LogoutView(generics.GenericAPIView):
|
||
"""
|
||
API endpoint для выхода пользователя.
|
||
|
||
POST /api/auth/logout/
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request, *args, **kwargs):
|
||
"""Выход пользователя (blacklist refresh токена)."""
|
||
try:
|
||
refresh_token = request.data.get('refresh')
|
||
if refresh_token:
|
||
token = RefreshToken(refresh_token)
|
||
token.blacklist()
|
||
|
||
return Response({
|
||
'success': True,
|
||
'message': 'Выход выполнен успешно'
|
||
}, status=status.HTTP_200_OK)
|
||
except Exception as e:
|
||
return Response({
|
||
'success': False,
|
||
'error': {
|
||
'message': 'Ошибка при выходе',
|
||
'details': str(e)
|
||
}
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
|
||
class ChangePasswordView(generics.GenericAPIView):
|
||
"""
|
||
API endpoint для смены пароля.
|
||
|
||
POST /api/auth/change-password/
|
||
"""
|
||
serializer_class = ChangePasswordSerializer
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request, *args, **kwargs):
|
||
"""Смена пароля пользователя."""
|
||
serializer = self.get_serializer(data=request.data)
|
||
serializer.is_valid(raise_exception=True)
|
||
|
||
user = request.user
|
||
|
||
# Проверяем старый пароль
|
||
if not user.check_password(serializer.validated_data['old_password']):
|
||
return Response({
|
||
'success': False,
|
||
'error': {
|
||
'old_password': ['Неверный пароль']
|
||
}
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# Устанавливаем новый пароль
|
||
user.set_password(serializer.validated_data['new_password'])
|
||
user.save()
|
||
|
||
return Response({
|
||
'success': True,
|
||
'message': 'Пароль успешно изменен'
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
|
||
class PasswordResetRequestView(generics.GenericAPIView):
|
||
"""
|
||
API endpoint для запроса восстановления пароля.
|
||
|
||
POST /api/auth/password-reset/
|
||
"""
|
||
serializer_class = PasswordResetRequestSerializer
|
||
permission_classes = [AllowAny]
|
||
authentication_classes = []
|
||
throttle_classes = [BurstRateThrottle]
|
||
|
||
def post(self, request, *args, **kwargs):
|
||
"""Отправка email для восстановления пароля."""
|
||
serializer = self.get_serializer(data=request.data)
|
||
serializer.is_valid(raise_exception=True)
|
||
|
||
email = serializer.validated_data['email']
|
||
|
||
try:
|
||
user = User.objects.get(email=email)
|
||
|
||
# Генерируем токен для сброса пароля
|
||
reset_token = secrets.token_urlsafe(32)
|
||
user.email_verification_token = reset_token
|
||
user.save()
|
||
|
||
# Отправляем email (асинхронно)
|
||
send_password_reset_email_task.delay(user.id, reset_token)
|
||
except User.DoesNotExist:
|
||
# Не раскрываем информацию о существовании email
|
||
pass
|
||
|
||
return Response({
|
||
'success': True,
|
||
'message': 'Если email существует, на него будет отправлено письмо с инструкциями'
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
|
||
class PasswordResetConfirmView(generics.GenericAPIView):
|
||
"""
|
||
API endpoint для подтверждения восстановления пароля.
|
||
|
||
POST /api/auth/password-reset-confirm/
|
||
"""
|
||
serializer_class = PasswordResetConfirmSerializer
|
||
permission_classes = [AllowAny]
|
||
authentication_classes = []
|
||
|
||
def post(self, request, *args, **kwargs):
|
||
"""Подтверждение восстановления пароля."""
|
||
serializer = self.get_serializer(data=request.data)
|
||
serializer.is_valid(raise_exception=True)
|
||
|
||
token = serializer.validated_data['token']
|
||
new_password = serializer.validated_data['new_password']
|
||
|
||
try:
|
||
user = User.objects.get(email_verification_token=token)
|
||
user.set_password(new_password)
|
||
user.email_verification_token = ''
|
||
user.save()
|
||
|
||
return Response({
|
||
'success': True,
|
||
'message': 'Пароль успешно изменен'
|
||
}, status=status.HTTP_200_OK)
|
||
except User.DoesNotExist:
|
||
return Response({
|
||
'success': False,
|
||
'error': {
|
||
'message': 'Неверный или истекший токен'
|
||
}
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
|
||
class EmailVerificationView(generics.GenericAPIView):
|
||
"""
|
||
API endpoint для подтверждения email.
|
||
|
||
POST /api/auth/verify-email/
|
||
"""
|
||
serializer_class = EmailVerificationSerializer
|
||
permission_classes = [AllowAny]
|
||
authentication_classes = []
|
||
|
||
def post(self, request, *args, **kwargs):
|
||
"""Подтверждение email пользователя."""
|
||
serializer = self.get_serializer(data=request.data)
|
||
serializer.is_valid(raise_exception=True)
|
||
|
||
token = serializer.validated_data['token']
|
||
|
||
try:
|
||
user = User.objects.get(email_verification_token=token)
|
||
user.email_verified = True
|
||
user.email_verification_token = ''
|
||
user.save()
|
||
|
||
# Отправляем приветственное письмо после подтверждения email
|
||
send_welcome_email_task.delay(user.id)
|
||
|
||
return Response({
|
||
'success': True,
|
||
'message': 'Email успешно подтвержден'
|
||
}, status=status.HTTP_200_OK)
|
||
except User.DoesNotExist:
|
||
return Response({
|
||
'success': False,
|
||
'error': {
|
||
'message': 'Неверный или истекший токен'
|
||
}
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
|
||
class ResendVerificationEmailView(generics.GenericAPIView):
|
||
"""
|
||
API endpoint для повторной отправки письма подтверждения email.
|
||
|
||
POST /api/auth/resend-verification/
|
||
Можно использовать с авторизацией или без (передавая email)
|
||
"""
|
||
permission_classes = [AllowAny]
|
||
authentication_classes = []
|
||
|
||
def post(self, request, *args, **kwargs):
|
||
"""Повторная отправка письма подтверждения email."""
|
||
# Если пользователь авторизован, используем его email
|
||
if request.user.is_authenticated:
|
||
user = request.user
|
||
else:
|
||
# Если не авторизован, получаем email из запроса
|
||
email = request.data.get('email')
|
||
if not email:
|
||
return Response({
|
||
'success': False,
|
||
'error': 'Email обязателен'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
user = User.objects.get(email=email)
|
||
except User.DoesNotExist:
|
||
# Не раскрываем информацию о существовании email
|
||
return Response({
|
||
'success': True,
|
||
'message': 'Если email существует, на него будет отправлено письмо с подтверждением'
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
# Если email уже подтвержден, не отправляем письмо
|
||
if user.email_verified:
|
||
return Response({
|
||
'success': False,
|
||
'message': 'Email уже подтвержден'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# Генерируем новый токен для подтверждения
|
||
verification_token = secrets.token_urlsafe(32)
|
||
user.email_verification_token = verification_token
|
||
user.save()
|
||
|
||
# Отправляем email подтверждения (асинхронно через Celery)
|
||
send_verification_email_task.delay(user.id, verification_token)
|
||
|
||
return Response({
|
||
'success': True,
|
||
'message': 'Письмо подтверждения отправлено на ваш email'
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
|
||
class UserViewSet(viewsets.ModelViewSet):
|
||
"""
|
||
ViewSet для управления пользователями.
|
||
"""
|
||
|
||
@action(detail=False, methods=['get'])
|
||
def export_data(self, request):
|
||
"""
|
||
Экспорт данных пользователя (GDPR compliance).
|
||
|
||
GET /api/users/export_data/?format=json
|
||
"""
|
||
from .services import DataExportService
|
||
|
||
format_type = request.query_params.get('format', 'json')
|
||
|
||
if format_type not in ['json']:
|
||
return Response(
|
||
{'error': 'Неподдерживаемый формат. Доступен только JSON.'},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
response = DataExportService.generate_export_file(request.user, format=format_type)
|
||
|
||
if not response:
|
||
return Response(
|
||
{'error': 'Ошибка при экспорте данных'},
|
||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||
)
|
||
|
||
return response
|
||
|
||
@action(detail=True, methods=['get'], url_path='avatar_url')
|
||
def avatar_url(self, request, pk=None):
|
||
"""
|
||
Получить URL аватара пользователя по id (для плейсхолдера в видеозвоне).
|
||
GET /api/users/<id>/avatar_url/
|
||
Доступно любому аутентифицированному пользователю.
|
||
"""
|
||
user = User.objects.filter(pk=pk).only('avatar').first()
|
||
if not user:
|
||
return Response({'avatar_url': None}, status=status.HTTP_404_NOT_FOUND)
|
||
avatar_url = None
|
||
if user.avatar:
|
||
avatar_url = request.build_absolute_uri(user.avatar.url)
|
||
return Response({'avatar_url': avatar_url})
|
||
|
||
@action(detail=False, methods=['get'])
|
||
def contacts(self, request):
|
||
"""
|
||
Получить список пользователей, с которыми текущий пользователь может начать чат.
|
||
|
||
Связи:
|
||
- Ментор <-> Студенты
|
||
- Студент <-> Менторы
|
||
- Ментор <-> Родители его студентов
|
||
- Родитель <-> Менторы его детей
|
||
"""
|
||
user = request.user
|
||
contact_ids = set()
|
||
|
||
if user.role == 'mentor':
|
||
# Студенты ментора
|
||
student_ids = User.objects.filter(role='client', client_profile__mentors=user).values_list('id', flat=True)
|
||
contact_ids.update(student_ids)
|
||
# Родители студентов ментора
|
||
parent_ids = User.objects.filter(role='parent', parent_profile__children__mentors=user).values_list('id', flat=True)
|
||
contact_ids.update(parent_ids)
|
||
|
||
elif user.role == 'client':
|
||
# Менторы студента
|
||
mentor_ids = User.objects.filter(role='mentor', clients__user=user).values_list('id', flat=True)
|
||
contact_ids.update(mentor_ids)
|
||
# Родители студента
|
||
parent_ids = User.objects.filter(role='parent', parent_profile__children__user=user).values_list('id', flat=True)
|
||
contact_ids.update(parent_ids)
|
||
|
||
elif user.role == 'parent':
|
||
# Менторы детей родителя
|
||
mentor_ids = User.objects.filter(role='mentor', clients__parents__user=user).values_list('id', flat=True)
|
||
contact_ids.update(mentor_ids)
|
||
# Дети родителя
|
||
child_ids = User.objects.filter(role='client', client_profile__parents__user=user).values_list('id', flat=True)
|
||
contact_ids.update(child_ids)
|
||
|
||
# Получаем пользователей по ID
|
||
contacts = User.objects.filter(id__in=contact_ids).distinct().only(
|
||
'id', 'email', 'first_name', 'last_name', 'role',
|
||
'phone', 'avatar', 'timezone', 'language',
|
||
'country', 'city', 'email_verified', 'is_active',
|
||
'created_at', 'last_activity'
|
||
)
|
||
|
||
# Поиск по имени или email
|
||
search = request.query_params.get('search')
|
||
if search:
|
||
contacts = contacts.filter(
|
||
Q(first_name__icontains=search) |
|
||
Q(last_name__icontains=search) |
|
||
Q(email__icontains=search)
|
||
)
|
||
|
||
# Пагинация
|
||
page = self.paginate_queryset(contacts)
|
||
if page is not None:
|
||
serializer = UserSerializer(page, many=True, context={'request': request})
|
||
return self.get_paginated_response(serializer.data)
|
||
|
||
serializer = UserSerializer(contacts, many=True, context={'request': request})
|
||
return Response(serializer.data)
|
||
|
||
queryset = User.objects.all()
|
||
serializer_class = UserSerializer
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def get_queryset(self):
|
||
"""Фильтрация пользователей в зависимости от роли."""
|
||
user = self.request.user
|
||
if user.is_superuser:
|
||
# Оптимизация: используем only() для ограничения полей в списке
|
||
return User.objects.all().only(
|
||
'id', 'email', 'first_name', 'last_name', 'role',
|
||
'phone', 'avatar', 'timezone', 'language',
|
||
'country', 'city', 'email_verified', 'is_active',
|
||
'created_at', 'last_activity'
|
||
)
|
||
elif user.role == 'mentor':
|
||
# Ментор видит только своих клиентов
|
||
# Оптимизация: используем select_related для избежания N+1 запросов
|
||
return User.objects.filter(
|
||
role='client',
|
||
clients__mentors=user
|
||
).select_related().distinct().only(
|
||
'id', 'email', 'first_name', 'last_name', 'role',
|
||
'phone', 'avatar', 'timezone', 'language',
|
||
'country', 'city', 'email_verified', 'is_active',
|
||
'created_at', 'last_activity'
|
||
)
|
||
else:
|
||
# Остальные видят только себя
|
||
return User.objects.filter(id=user.id)
|
||
|
||
|
||
class ClientViewSet(viewsets.ModelViewSet):
|
||
"""
|
||
ViewSet для управления клиентами.
|
||
"""
|
||
queryset = Client.objects.all()
|
||
serializer_class = ClientSerializer
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def get_queryset(self):
|
||
"""Фильтрация клиентов в зависимости от роли."""
|
||
user = self.request.user
|
||
if user.is_superuser:
|
||
queryset = Client.objects.all()
|
||
elif user.role == 'mentor':
|
||
# Ментор видит только своих клиентов
|
||
queryset = Client.objects.filter(mentors=user)
|
||
else:
|
||
# Клиент видит только свой профиль
|
||
queryset = Client.objects.filter(user=user)
|
||
|
||
# Оптимизация: используем select_related и prefetch_related для избежания N+1
|
||
queryset = queryset.select_related('user').prefetch_related('mentors')
|
||
|
||
# Оптимизация: для списка используем only() для ограничения полей
|
||
if self.action == 'list':
|
||
queryset = queryset.only(
|
||
'id', 'user_id', 'grade', 'school', 'learning_goals',
|
||
'enrollment_date', 'created_at'
|
||
)
|
||
|
||
return queryset
|
||
|
||
def create(self, request, *args, **kwargs):
|
||
"""Создание клиента с проверкой необходимости доплаты."""
|
||
response = super().create(request, *args, **kwargs)
|
||
|
||
# Проверяем необходимость доплаты для менторов с тарифом "За ученика"
|
||
if request.user.role == 'mentor':
|
||
from apps.subscriptions.services import SubscriptionService
|
||
subscription = SubscriptionService.get_active_subscription(request.user)
|
||
|
||
if subscription and subscription.plan.subscription_type == 'per_student':
|
||
# Получаем текущее количество клиентов
|
||
current_count = Client.objects.filter(mentors=request.user).count()
|
||
|
||
# Если превышен лимит оплаченных учеников
|
||
if current_count > subscription.student_count:
|
||
# Рассчитываем доплату
|
||
payment_data = SubscriptionService.calculate_extra_students_payment(
|
||
subscription=subscription,
|
||
new_student_count=current_count
|
||
)
|
||
|
||
# Добавляем информацию о необходимости доплаты в ответ
|
||
response.data['requires_payment'] = True
|
||
response.data['payment_info'] = {
|
||
'extra_students': payment_data['extra_students'],
|
||
'price_per_student': float(payment_data['price_per_student']),
|
||
'days_remaining': payment_data['days_remaining'],
|
||
'total_days': payment_data['total_days'],
|
||
'payment_amount': float(payment_data['payment_amount']),
|
||
'next_month_amount': float(payment_data['next_month_amount']),
|
||
'current_student_count': payment_data['current_student_count'],
|
||
'new_student_count': payment_data['new_student_count']
|
||
}
|
||
else:
|
||
response.data['requires_payment'] = False
|
||
|
||
return response
|
||
|
||
|
||
class ParentViewSet(viewsets.ModelViewSet):
|
||
"""
|
||
ViewSet для управления родителями.
|
||
"""
|
||
queryset = Parent.objects.all()
|
||
serializer_class = ParentSerializer
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def get_queryset(self):
|
||
"""Фильтрация родителей в зависимости от роли."""
|
||
user = self.request.user
|
||
if user.is_superuser:
|
||
queryset = Parent.objects.all()
|
||
elif user.role == 'mentor':
|
||
# Ментор видит родителей своих клиентов
|
||
# children - это ManyToManyField к Client, mentors - ManyToManyField к User в Client
|
||
queryset = Parent.objects.filter(
|
||
children__mentors=user
|
||
).distinct()
|
||
else:
|
||
# Родитель видит только свой профиль
|
||
queryset = Parent.objects.filter(user=user)
|
||
|
||
# Оптимизация: используем select_related и prefetch_related для избежания N+1
|
||
queryset = queryset.select_related('user').prefetch_related('children', 'children__user')
|
||
|
||
# Оптимизация: для списка используем only() для ограничения полей
|
||
if self.action == 'list':
|
||
queryset = queryset.only(
|
||
'id', 'user_id', 'relation_type', 'can_view_progress',
|
||
'can_view_schedule', 'can_receive_reports', 'created_at'
|
||
)
|
||
|
||
return queryset
|
||
|
||
|
||
class GroupViewSet(viewsets.ModelViewSet):
|
||
"""
|
||
ViewSet для управления группами.
|
||
"""
|
||
queryset = Group.objects.all()
|
||
serializer_class = GroupSerializer
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def get_queryset(self):
|
||
"""Фильтрация групп в зависимости от роли."""
|
||
user = self.request.user
|
||
if user.is_superuser:
|
||
queryset = Group.objects.all()
|
||
elif user.role == 'mentor':
|
||
# Ментор видит только свои группы
|
||
queryset = Group.objects.filter(mentor=user)
|
||
elif user.role == 'client':
|
||
# Клиент видит только группы, в которых он состоит
|
||
try:
|
||
client = Client.objects.get(user=user)
|
||
queryset = Group.objects.filter(students=client)
|
||
except Client.DoesNotExist:
|
||
return Group.objects.none()
|
||
else:
|
||
return Group.objects.none()
|
||
|
||
# Оптимизация: используем select_related и prefetch_related для избежания N+1
|
||
queryset = queryset.select_related('mentor').prefetch_related('students', 'students__user')
|
||
|
||
# Оптимизация: для списка добавляем аннотации для подсчета студентов и уроков
|
||
if self.action == 'list':
|
||
from django.db.models import Count, Q
|
||
from apps.schedule.models import Lesson
|
||
|
||
queryset = queryset.annotate(
|
||
students_count_annotated=Count('students', distinct=True),
|
||
scheduled_lessons_annotated=Count(
|
||
'lessons',
|
||
filter=Q(lessons__status__in=['scheduled', 'in_progress']) & ~Q(lessons__status='cancelled'),
|
||
distinct=True
|
||
),
|
||
completed_lessons_annotated=Count(
|
||
'lessons',
|
||
filter=Q(lessons__status='completed') & ~Q(lessons__status='cancelled'),
|
||
distinct=True
|
||
)
|
||
).only(
|
||
'id', 'name', 'description', 'mentor_id',
|
||
'created_at'
|
||
)
|
||
|
||
return queryset
|
||
|
||
def list(self, request, *args, **kwargs):
|
||
"""
|
||
Список групп с кешированием.
|
||
"""
|
||
user = request.user
|
||
|
||
# Кеширование: кеш на 2 минуты для каждого пользователя и страницы
|
||
page = int(request.query_params.get('page', 1))
|
||
page_size = int(request.query_params.get('page_size', 20))
|
||
cache_key = f'groups_{user.id}_{page}_{page_size}'
|
||
|
||
from django.core.cache import cache
|
||
cached_data = cache.get(cache_key)
|
||
|
||
if cached_data is not None:
|
||
return Response(cached_data)
|
||
|
||
# Вызываем родительский метод для получения данных
|
||
response = super().list(request, *args, **kwargs)
|
||
|
||
# Сохраняем в кеш на 2 минуты (120 секунд)
|
||
cache.set(cache_key, response.data, 120)
|
||
|
||
return response
|