uchill/backend/apps/users/mentorship_views.py

332 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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):
"""После принятия связи: добавить ментора к студенту, создать доску."""
from django.core.cache import cache
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'])
# Инвалидируем кэш списка студентов ментора
for page in range(1, 6):
for page_size in [10, 20, 50]:
cache.delete(f'manage_clients_{mentor.id}_{page}_{page_size}')
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)