""" API для запросов на менторство. Студент отправляет запрос по коду ментора, ментор принимает или отклоняет. """ from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from django.core.cache import cache from .models import User, Client, MentorStudentConnection from apps.notifications.services import NotificationService from apps.board.models import Board def _apply_connection(conn): """После принятия связи: добавить ментора к студенту, создать доску.""" student_user = conn.student mentor = conn.mentor try: client = student_user.client_profile except Client.DoesNotExist: client = Client.objects.create(user=student_user) if mentor not in client.mentors.all(): client.mentors.add(mentor) board, _ = Board.objects.get_or_create( mentor=mentor, student=student_user, access_type='mentor_student', defaults={ 'title': 'Доска для совместной работы', 'description': f'Интерактивная доска для {student_user.get_full_name()}', 'owner': mentor, } ) if board: board.participants.add(mentor, student_user) if conn.status != MentorStudentConnection.STATUS_ACCEPTED: conn.status = MentorStudentConnection.STATUS_ACCEPTED conn.save(update_fields=['status', 'updated_at']) class MentorshipRequestViewSet(viewsets.ViewSet): """ Запросы на менторство: студент отправляет по коду ментора, ментор принимает/отклоняет. send: POST - студент отправляет запрос (mentor_code) pending: GET - ментор получает список ожидающих запросов accept: POST - ментор принимает reject: POST - ментор отклоняет my_requests: GET - студент получает свои отправленные запросы """ permission_classes = [IsAuthenticated] @action(detail=False, methods=['post']) def send(self, request): """ Студент отправляет запрос на менторство по коду ментора. POST /api/mentorship-requests/send/ Body: { "mentor_code": "ABC12345" } """ user = request.user if user.role != 'client': return Response( {'error': 'Только для учеников'}, status=status.HTTP_403_FORBIDDEN ) mentor_code = (request.data.get('mentor_code') or request.data.get('mentorCode') or '').strip().upper() if not mentor_code: return Response( {'error': 'Укажите код ментора'}, status=status.HTTP_400_BAD_REQUEST ) allowed = set('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') valid = len(mentor_code) == 8 and all(c in allowed for c in mentor_code) if not valid: return Response( {'error': 'Код ментора: 8 символов (цифры и латинские буквы)'}, status=status.HTTP_400_BAD_REQUEST ) try: mentor = User.objects.get(universal_code=mentor_code, role='mentor') except User.DoesNotExist: return Response( {'error': 'Ментор с таким кодом не найден'}, status=status.HTTP_404_NOT_FOUND ) # Проверка: уже студент этого ментора try: client = user.client_profile except Client.DoesNotExist: client = Client.objects.create(user=user) if mentor in client.mentors.all(): return Response( {'error': 'Вы уже являетесь учеником этого ментора'}, status=status.HTTP_400_BAD_REQUEST ) # Проверка: есть ли уже ожидающий запрос pending = MentorStudentConnection.objects.filter( student=user, mentor=mentor, status=MentorStudentConnection.STATUS_PENDING_MENTOR, ).exists() if pending: return Response( {'error': 'Запрос уже отправлен, ожидайте ответа ментора'}, status=status.HTTP_400_BAD_REQUEST ) conn = MentorStudentConnection.objects.create( student=user, mentor=mentor, status=MentorStudentConnection.STATUS_PENDING_MENTOR, initiator=MentorStudentConnection.INITIATOR_STUDENT, ) # Уведомление ментору (in-app, email, telegram) NotificationService.create_notification_with_telegram( recipient=mentor, notification_type='mentorship_request_new', title='Новый запрос на менторство', message=f'{user.get_full_name() or user.email} отправил(а) запрос на связь с вами.', action_url='/students?tab=requests', data={'mentorship_request_id': conn.id, 'connection_id': conn.id}, ) return Response({ 'id': conn.id, 'status': conn.status, 'mentor': { 'id': mentor.id, 'first_name': mentor.first_name, 'last_name': mentor.last_name, 'email': mentor.email, }, 'message': 'Запрос отправлен. Ожидайте ответа ментора.', }, status=status.HTTP_201_CREATED) @action(detail=False, methods=['get']) def pending(self, request): """ Ментор получает список ожидающих запросов. GET /api/mentorship-requests/pending/ """ user = request.user if user.role != 'mentor': return Response( {'error': 'Только для менторов'}, status=status.HTTP_403_FORBIDDEN ) conns = MentorStudentConnection.objects.filter( mentor=user, status=MentorStudentConnection.STATUS_PENDING_MENTOR, initiator=MentorStudentConnection.INITIATOR_STUDENT, ).select_related('student').order_by('-created_at') data = [] for req in conns: try: client_id = req.student.client_profile.id except Client.DoesNotExist: client_id = None data.append({ 'id': req.id, 'status': req.status, 'created_at': req.created_at.isoformat() if req.created_at else None, 'student': { 'id': client_id, 'user_id': req.student.id, 'email': req.student.email, 'first_name': req.student.first_name or '', 'last_name': req.student.last_name or '', 'avatar': req.student.avatar.url if req.student.avatar else None, }, }) return Response(data) @action(detail=True, methods=['post']) def accept(self, request, pk=None): """ Ментор принимает запрос. POST /api/mentorship-requests/{id}/accept/ """ user = request.user if user.role != 'mentor': return Response( {'error': 'Только для менторов'}, status=status.HTTP_403_FORBIDDEN ) try: req = MentorStudentConnection.objects.select_related('student', 'mentor').get( id=pk, mentor=user, status=MentorStudentConnection.STATUS_PENDING_MENTOR, initiator=MentorStudentConnection.INITIATOR_STUDENT, ) except MentorStudentConnection.DoesNotExist: return Response( {'error': 'Запрос не найден или уже обработан'}, status=status.HTTP_404_NOT_FOUND ) req.status = MentorStudentConnection.STATUS_ACCEPTED req.save(update_fields=['status', 'updated_at']) _apply_connection(req) # Уведомление студенту (in-app, email, telegram) NotificationService.create_notification_with_telegram( recipient=req.student, notification_type='mentorship_request_accepted', title='Запрос на менторство принят', message=f'{user.get_full_name() or user.email} принял(а) ваш запрос на связь.', action_url='/request-mentor', data={'mentorship_request_id': req.id, 'connection_id': req.id}, ) # Инвалидация кеша списка клиентов for page in range(1, 10): for size in [10, 20, 50, 100, 1000]: cache.delete(f'manage_clients_{user.id}_{page}_{size}') return Response({ 'status': 'accepted', 'message': 'Запрос принят. Студент добавлен в ваш список.', }) @action(detail=True, methods=['post']) def reject(self, request, pk=None): """ Ментор отклоняет запрос. Студент сможет отправить запрос повторно. POST /api/mentorship-requests/{id}/reject/ """ user = request.user if user.role != 'mentor': return Response( {'error': 'Только для менторов'}, status=status.HTTP_403_FORBIDDEN ) try: req = MentorStudentConnection.objects.select_related('student').get( id=pk, mentor=user, status=MentorStudentConnection.STATUS_PENDING_MENTOR, initiator=MentorStudentConnection.INITIATOR_STUDENT, ) except MentorStudentConnection.DoesNotExist: return Response( {'error': 'Запрос не найден или уже обработан'}, status=status.HTTP_404_NOT_FOUND ) req.status = MentorStudentConnection.STATUS_REJECTED req.save(update_fields=['status', 'updated_at']) # Уведомление студенту (in-app, email, telegram) NotificationService.create_notification_with_telegram( recipient=req.student, notification_type='mentorship_request_rejected', title='Запрос на менторство отклонён', message=f'{user.get_full_name() or user.email} отклонил(а) ваш запрос на связь. Вы можете отправить запрос повторно.', action_url='/request-mentor', data={'mentorship_request_id': req.id}, ) return Response({ 'status': 'rejected', 'message': 'Запрос отклонён.', }) @action(detail=False, methods=['get'], url_path='my-requests') def my_requests(self, request): """ Студент получает список своих отправленных запросов. Не показывает менторов, от которых студент отключён (связь убрана в админке). GET /api/mentorship-requests/my-requests/ """ user = request.user if user.role != 'client': return Response( {'error': 'Только для учеников'}, status=status.HTTP_403_FORBIDDEN ) conns = MentorStudentConnection.objects.filter( student=user, initiator=MentorStudentConnection.INITIATOR_STUDENT, ).select_related('mentor').order_by('-created_at')[:50] try: client = user.client_profile mentor_ids = set(client.mentors.values_list('id', flat=True)) except Client.DoesNotExist: mentor_ids = set() data = [] for req in conns: if req.status == MentorStudentConnection.STATUS_ACCEPTED and req.mentor_id not in mentor_ids: continue data.append({ 'id': req.id, 'status': req.status, 'created_at': req.created_at.isoformat() if req.created_at else None, 'updated_at': req.updated_at.isoformat() if req.updated_at else None, 'mentor': { 'id': req.mentor.id, 'email': req.mentor.email, 'first_name': req.mentor.first_name, 'last_name': req.mentor.last_name, }, }) return Response(data) @action(detail=False, methods=['get'], url_path='my-mentors') def my_mentors(self, request): """ Список менторов студента (из client.mentors). Включает менторов, подключённых через приглашение или принятый запрос. GET /api/mentorship-requests/my-mentors/ """ user = request.user if user.role != 'client': return Response( {'error': 'Только для учеников'}, status=status.HTTP_403_FORBIDDEN ) try: client = user.client_profile mentors = client.mentors.all().order_by('first_name', 'last_name') except Client.DoesNotExist: mentors = [] data = [ { 'id': m.id, 'email': m.email, 'first_name': m.first_name, 'last_name': m.last_name, 'avatar_url': request.build_absolute_uri(m.avatar.url) if m.avatar else None, } for m in mentors ] return Response(data)