""" API views для домашних заданий. """ import logging import html 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.db import models from django.utils import timezone from .models import Homework, HomeworkSubmission, HomeworkFile, HomeworkAIAgent from .serializers import ( HomeworkSerializer, HomeworkListSerializer, HomeworkCreateSerializer, HomeworkSubmissionSerializer, HomeworkSubmissionCreateSerializer, HomeworkGradeSerializer, HomeworkReturnSerializer, HomeworkFileSerializer, HomeworkAIAgentSerializer, ) from .permissions import IsHomeworkMentor, IsSubmissionOwnerOrMentor from django.contrib.auth import get_user_model from config.throttling import UploadRateThrottle User = get_user_model() logger = logging.getLogger(__name__) def _student_display_name(submission): """Имя ученика (только имя, без фамилии) — тот, кто отправил ДЗ.""" if not submission or not getattr(submission, 'student', None): return None s = submission.student name = (s.first_name or (s.get_short_name() if getattr(s, 'email', None) else '') or '').strip() return name or f"Студент (id: {s.pk})" class HomeworkViewSet(viewsets.ModelViewSet): """ ViewSet для управления домашними заданиями. list: Список ДЗ create: Создать ДЗ retrieve: Получить ДЗ update: Обновить ДЗ destroy: Удалить ДЗ publish: Опубликовать ДЗ archive: Архивировать ДЗ statistics: Статистика ДЗ """ permission_classes = [IsAuthenticated] def get_throttles(self): """Применяем throttling только для создания (загрузки файлов).""" if self.action == 'create': return [UploadRateThrottle()] return super().get_throttles() def get_permissions(self): """Определение прав доступа для разных действий.""" if self.action in ['update', 'partial_update', 'destroy']: return [IsAuthenticated(), IsHomeworkMentor()] return [IsAuthenticated()] def get_queryset(self): """Получение ДЗ.""" user = self.request.user filter_status = self.request.query_params.get('status') if user.role == 'mentor': # Ментор видит свои задания queryset = Homework.objects.filter( mentor=user ).select_related('mentor', 'lesson').prefetch_related( 'assigned_to', 'submissions__student', 'files', 'assignment_files' # Файлы задания (прямая связь) ) elif user.role == 'parent': # Родитель видит задания своих детей from apps.users.models import Parent, Client try: parent = Parent.objects.get(user=user) children_ids = parent.children.values_list('user_id', flat=True) # Если указан child_id (user_id ребенка), фильтруем по конкретному ребенку child_id = self.request.query_params.get('child_id') if child_id: try: # Проверяем, что это ребенок родителя child_client = Client.objects.get(user_id=child_id) if child_client.id in parent.children.values_list('id', flat=True): # Фильтруем задания, назначенные этому ребенку queryset = Homework.objects.filter( assigned_to=child_client.user, status='published' ).select_related('mentor', 'lesson').prefetch_related( 'assigned_to', 'submissions__student', 'files', 'assignment_files' ) else: queryset = Homework.objects.none() except (Client.DoesNotExist, ValueError): queryset = Homework.objects.none() else: # Если child_id не указан, показываем задания всех детей queryset = Homework.objects.filter( assigned_to__id__in=children_ids, status='published' ).select_related('mentor', 'lesson').prefetch_related( 'assigned_to', 'submissions__student', 'files', 'assignment_files' ).distinct() except Parent.DoesNotExist: queryset = Homework.objects.none() else: # Студент видит назначенные ему задания queryset = Homework.objects.filter( assigned_to=user, status='published' ).select_related('mentor', 'lesson').prefetch_related( 'assigned_to', 'submissions__student', 'files', 'assignment_files' # Файлы задания (прямая связь) ) # Оптимизация: для списка используем only() для ограничения полей if self.action == 'list': queryset = queryset.only( 'id', 'title', 'description', 'mentor_id', 'lesson_id', 'deadline', 'max_score', 'passing_score', 'status', 'fill_later', 'total_submissions', 'checked_submissions', 'returned_submissions', 'ai_draft_submissions', 'average_score', 'created_at', 'updated_at', 'published_at' ) # Фильтрация по статусу submissions if filter_status: if filter_status == 'pending': # Ожидают: нет решений или все решения в статусе pending if user.role == 'mentor': # Для ментора: задания, где нет решений вообще queryset = queryset.filter( submissions__isnull=True ).distinct() else: # Для студента: задания без его решений queryset = queryset.exclude( submissions__student=user ).distinct() elif filter_status == 'submitted': # На проверке: есть решения в статусе checking или pending (но не graded) if user.role == 'mentor': # Для ментора: задания с решениями в статусе checking или pending queryset = queryset.filter( submissions__status__in=['checking', 'pending'] ).exclude( submissions__status='graded' ).distinct() else: # Для студента: его решения в статусе checking или pending queryset = queryset.filter( submissions__student=user, submissions__status__in=['checking', 'pending'] ).distinct() elif filter_status == 'reviewed': # Проверено: есть решения в статусе graded if user.role == 'mentor': # Для ментора: задания с хотя бы одним решением в статусе graded queryset = queryset.filter( submissions__status='graded' ).distinct() else: # Для студента: его решения в статусе graded queryset = queryset.filter( submissions__student=user, submissions__status='graded' ).distinct() return queryset def get_serializer_class(self): """Выбор сериализатора.""" if self.action == 'list': return HomeworkListSerializer elif self.action == 'create': return HomeworkCreateSerializer elif self.action in ['update', 'partial_update']: return HomeworkCreateSerializer # Используем тот же сериализатор для обновления return HomeworkSerializer def create(self, request, *args, **kwargs): """Создание ДЗ.""" serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) homework = serializer.save() # Инвалидируем кеш дашборда после создания ДЗ from apps.users.cache_utils import invalidate_dashboard_cache invalidate_dashboard_cache(homework.mentor.id, 'mentor') # Оптимизация: используем list() для кеширования запроса students = list(homework.assigned_to.all()) for student in students: invalidate_dashboard_cache(student.id, 'client') # Отправляем уведомление о новом ДЗ from apps.notifications.services import NotificationService NotificationService.send_homework_notification(homework, 'homework_assigned') response_serializer = HomeworkSerializer(homework) return Response( response_serializer.data, status=status.HTTP_201_CREATED ) @action(detail=True, methods=['post']) def publish(self, request, pk=None): """ Опубликовать ДЗ. POST /api/homework/homeworks/{id}/publish/ """ homework = self.get_object() # Проверяем права if homework.mentor != request.user: return Response( {'error': 'Только автор может опубликовать задание'}, status=status.HTTP_403_FORBIDDEN ) homework.publish() # Инвалидируем кеш дашборда после публикации ДЗ from apps.users.cache_utils import invalidate_dashboard_cache invalidate_dashboard_cache(homework.mentor.id, 'mentor') # Оптимизация: используем list() для кеширования запроса students = list(homework.assigned_to.all()) for student in students: invalidate_dashboard_cache(student.id, 'client') # Отправляем уведомления студентам from apps.notifications.services import NotificationService for student in students: NotificationService.create_notification_with_telegram( recipient=student, notification_type='homework_assigned', title='📚 Новое домашнее задание', message=f'Новое домашнее задание: "{homework.title}"', priority='normal', action_url=f'/homework/{homework.id}/', content_object=homework ) serializer = HomeworkSerializer(homework) return Response(serializer.data) @action(detail=True, methods=['post']) def archive(self, request, pk=None): """ Архивировать ДЗ. POST /api/homework/homeworks/{id}/archive/ """ homework = self.get_object() # Проверяем права if homework.mentor != request.user: return Response( {'error': 'Только автор может архивировать задание'}, status=status.HTTP_403_FORBIDDEN ) homework.archive() serializer = HomeworkSerializer(homework) return Response(serializer.data) @action(detail=True, methods=['get']) def statistics(self, request, pk=None): """ Получить статистику ДЗ. GET /api/homework/homeworks/{id}/statistics/ """ homework = self.get_object() # Проверяем права if homework.mentor != request.user: return Response( {'error': 'Только автор может просмотреть статистику'}, status=status.HTTP_403_FORBIDDEN ) # Оптимизация: используем один запрос с агрегацией вместо множества отдельных count() from django.db.models import Count, Q, Avg submissions_queryset = homework.submissions.all() # Получаем все подсчеты одним запросом через агрегацию aggregated = submissions_queryset.aggregate( total_submissions=Count('id'), unique_students=Count('student', distinct=True), pending=Count('id', filter=Q(status='pending')), checking=Count('id', filter=Q(status='checking')), graded=Count('id', filter=Q(status='graded')), returned=Count('id', filter=Q(status='returned')), passed=Count('id', filter=Q(passed=True)), failed=Count('id', filter=Q(passed=False, status='graded')), on_time=Count('id', filter=Q(is_late=False)), late=Count('id', filter=Q(is_late=True)), ) # Оптимизация: используем count() только один раз total_assigned = homework.assigned_to.count() stats = { 'total_assigned': total_assigned, 'total_submissions': aggregated['total_submissions'], 'unique_students': aggregated['unique_students'], 'pending': aggregated['pending'], 'checking': aggregated['checking'], 'graded': aggregated['graded'], 'returned': aggregated['returned'], 'passed': aggregated['passed'], 'failed': aggregated['failed'], 'average_score': homework.average_score, 'on_time': aggregated['on_time'], 'late': aggregated['late'], } # Распределение баллов - оптимизируем через один запрос с фильтрацией graded_submissions = submissions_queryset.filter(status='graded').values_list('score', flat=True) if graded_submissions.exists(): score_distribution = [] # Получаем все оценки одним запросом и считаем в Python scores = list(graded_submissions) for i in range(0, homework.max_score + 1, 10): count = sum(1 for score in scores if score is not None and i <= score < i + 10) score_distribution.append({ 'range': f'{i}-{i+9}', 'count': count }) stats['score_distribution'] = score_distribution return Response(stats) @action(detail=False, methods=['get']) def my_homeworks(self, request): """ Мои домашние задания (для студентов). GET /api/homework/homeworks/my_homeworks/ """ user = request.user homeworks = Homework.objects.filter( assigned_to=user, status='published' ).select_related('mentor', 'lesson').prefetch_related( 'assigned_to', 'submissions__student' ).only( 'id', 'title', 'description', 'mentor_id', 'lesson_id', 'deadline', 'max_score', 'passing_score', 'status', 'fill_later', 'total_submissions', 'checked_submissions', 'average_score', 'created_at', 'updated_at', 'published_at' ) serializer = HomeworkListSerializer(homeworks, many=True, context={'request': request}) return Response(serializer.data) @action(detail=False, methods=['get']) def created_by_me(self, request): """ Созданные мной задания (для менторов). GET /api/homework/homeworks/created_by_me/ """ homeworks = Homework.objects.filter( mentor=request.user ).select_related('mentor', 'lesson').prefetch_related( 'assigned_to', 'submissions__student' ).only( 'id', 'title', 'description', 'mentor_id', 'lesson_id', 'deadline', 'max_score', 'passing_score', 'status', 'fill_later', 'total_submissions', 'checked_submissions', 'average_score', 'created_at', 'updated_at', 'published_at' ) serializer = HomeworkListSerializer(homeworks, many=True, context={'request': request}) return Response(serializer.data) class HomeworkSubmissionViewSet(viewsets.ModelViewSet): """ ViewSet для управления решениями ДЗ. list: Список решений create: Создать решение retrieve: Получить решение update: Обновить решение destroy: Удалить решение grade: Выставить оценку return_for_revision: Вернуть на доработку """ permission_classes = [IsAuthenticated, IsSubmissionOwnerOrMentor] def get_queryset(self): """Получение решений.""" user = self.request.user homework_id = self.request.query_params.get('homework_id') child_id = self.request.query_params.get('child_id') queryset = HomeworkSubmission.objects.all() # Оптимизация: для списка используем только select_related для необходимых полей # и only() для ограничения полей (но без конфликтующих полей) if self.action == 'list': queryset = queryset.select_related( 'homework', 'homework__mentor', 'student' ).prefetch_related('files').only( 'id', 'homework_id', 'student_id', 'checked_by_id', 'status', 'score', 'passed', 'is_late', 'submitted_at', 'checked_at', 'feedback', 'updated_at' ) else: # Для детального просмотра используем полный select_related queryset = queryset.select_related( 'homework', 'homework__mentor', 'student', 'checked_by' ).prefetch_related('files') # Фильтр по заданию if homework_id: queryset = queryset.filter(homework_id=homework_id) # Ментор видит решения своих заданий if user.role == 'mentor': queryset = queryset.filter(homework__mentor=user) elif user.role == 'parent' and child_id: # Родитель с child_id видит решения выбранного ребёнка try: from apps.users.models import Client, Parent parent_profile = Parent.objects.get(user=user) child_client = Client.objects.get(user_id=child_id) if child_client in parent_profile.children.all(): queryset = queryset.filter(student=child_client.user) else: queryset = queryset.none() except (Client.DoesNotExist, Parent.DoesNotExist, ValueError): queryset = queryset.none() else: # Студент видит только свои решения queryset = queryset.filter(student=user) return queryset def get_serializer_class(self): """Выбор сериализатора.""" if self.action == 'create': return HomeworkSubmissionCreateSerializer elif self.action == 'grade': return HomeworkGradeSerializer elif self.action == 'return_for_revision': return HomeworkReturnSerializer return HomeworkSubmissionSerializer def create(self, request, *args, **kwargs): """Создание решения.""" # Получаем все файлы из запроса (может быть несколько с одинаковым именем поля) files = request.FILES.getlist('attachment') # Для сериализатора оставляем только первый файл (если есть) # Остальные обработаем после создания submission if files: request.data._mutable = True request.data['attachment'] = files[0] request.data._mutable = False # Проверяем, есть ли submission со статусом 'returned' для обновления homework_id = request.data.get('homework_id') from .models import HomeworkSubmission, HomeworkFile returned_submission_id = None if homework_id: try: returned_submission = HomeworkSubmission.objects.filter( homework_id=homework_id, student=request.user, status='returned' ).order_by('-submitted_at').first() if returned_submission: returned_submission_id = returned_submission.id except: pass serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) submission = serializer.save() # Если обновляется существующее submission (было возвращено на доработку), # удаляем старые дополнительные файлы if returned_submission_id and submission.id == returned_submission_id: old_files = HomeworkFile.objects.filter( submission=submission, file_type='submission' ) if old_files.exists(): # Удаляем старые файлы при перезаписи submission old_files.delete() # Сохраняем остальные файлы как HomeworkFile if len(files) > 1: for file in files[1:]: HomeworkFile.objects.create( submission=submission, file_type='submission', file=file, filename=file.name, file_size=file.size, uploaded_by=request.user ) # Инвалидируем кеш дашборда после создания решения ДЗ from apps.users.cache_utils import invalidate_dashboard_cache invalidate_dashboard_cache(submission.student.id, 'client') invalidate_dashboard_cache(submission.homework.mentor.id, 'mentor') # Отправляем уведомление ментору from apps.notifications.services import NotificationService NotificationService.create_notification_with_telegram( recipient=submission.homework.mentor, notification_type='homework_submitted', title='📝 ДЗ сдано', message=f'{submission.student.get_full_name()} сдал ДЗ "{submission.homework.title}"', priority='normal', action_url=f'/homework/{submission.homework.id}/submissions/{submission.id}/', content_object=submission ) # По настройкам ментора: доверять AI — запускаем проверку (черновик или публикация) mentor = submission.homework.mentor mentor.refresh_from_db(fields=['ai_trust_draft', 'ai_trust_publish']) if getattr(mentor, 'ai_trust_publish', False): from .tasks import run_mentor_ai_check_submission logger.info( "ДЗ: запуск AI проверки (публикация) для submission_id=%s, mentor_id=%s", submission.id, mentor.id ) run_mentor_ai_check_submission.delay(submission.id, publish=True) elif getattr(mentor, 'ai_trust_draft', False): from .tasks import run_mentor_ai_check_submission logger.info( "ДЗ: запуск AI проверки (черновик) для submission_id=%s, mentor_id=%s", submission.id, mentor.id ) run_mentor_ai_check_submission.delay(submission.id, publish=False) response_serializer = HomeworkSubmissionSerializer(submission) return Response( response_serializer.data, status=status.HTTP_201_CREATED ) @action(detail=True, methods=['post']) def check_with_ai(self, request, pk=None): """ Проверить решение через AI. POST /api/homework/submissions/{id}/check_with_ai/ """ submission = self.get_object() # Проверяем права доступа (только ментор) if request.user.role != 'mentor': return Response( {'error': 'Только ментор может использовать AI проверку'}, status=status.HTTP_403_FORBIDDEN ) # Проверяем что это задание ментора if submission.homework.mentor != request.user: return Response( {'error': 'Вы не можете проверять это задание'}, status=status.HTTP_403_FORBIDDEN ) # Импортируем сервис from .ai_service import get_ai_service from django.core.files.storage import default_storage # Файлы задания: имена, пути (если есть), содержимое (если path недоступен — S3 и т.д.) homework_files = [] homework_file_paths = [] homework_file_contents = [] def _add_homework_file(name): homework_files.append(name) path_ok = False try: p = default_storage.path(name) if p: homework_file_paths.append(p) path_ok = True except Exception: pass if not path_ok: try: with default_storage.open(name, 'rb') as f: data = f.read(2 * 1024 * 1024 + 1) # лимит 2 МБ fname = name.split('/')[-1] if '/' in name else name homework_file_contents.append((fname, data[: 2 * 1024 * 1024])) except Exception: pass if submission.homework.attachment: _add_homework_file(submission.homework.attachment.name) for f in submission.homework.assignment_files.all(): if f.file: _add_homework_file(f.file.name) # Файлы решения submission_files = [] submission_file_paths = [] submission_file_contents = [] def _add_submission_file(name): submission_files.append(name) path_ok = False try: p = default_storage.path(name) if p: submission_file_paths.append(p) path_ok = True except Exception: pass if not path_ok: try: with default_storage.open(name, 'rb') as f: data = f.read(2 * 1024 * 1024 + 1) fname = name.split('/')[-1] if '/' in name else name submission_file_contents.append((fname, data[: 2 * 1024 * 1024])) except Exception: pass if submission.attachment: _add_submission_file(submission.attachment.name) for file_obj in HomeworkFile.objects.filter(submission=submission, file_type='submission'): if file_obj.file: _add_submission_file(file_obj.file.name) # Проверка через ИИ: задание (текст + файлы), решение (текст + файлы) → комментарий и оценка 1–5 ai_service = get_ai_service() result = ai_service.check_submission( homework_title=submission.homework.title, homework_description=submission.homework.description or '', homework_max_score=5, submission_content=submission.content or '', submission_files=submission_files, homework_files=homework_files, homework_file_paths=homework_file_paths, submission_file_paths=submission_file_paths, homework_file_contents=homework_file_contents, submission_file_contents=submission_file_contents, student_name=_student_display_name(submission), ) if not result.get('success'): return Response( {'error': result.get('error', 'Ошибка AI проверки')}, status=status.HTTP_400_BAD_REQUEST ) # Сохраняем результат AI проверки submission.ai_score = result.get('score') submission.ai_feedback = result.get('feedback') submission.ai_checked_at = timezone.now() submission.save(update_fields=['ai_score', 'ai_feedback', 'ai_checked_at']) from .utils import feedback_to_html payload = { 'success': True, 'ai_score': submission.ai_score, 'ai_feedback': submission.ai_feedback, 'ai_feedback_html': feedback_to_html(submission.ai_feedback or ''), 'ai_checked_at': submission.ai_checked_at, 'message': 'AI проверка завершена успешно' } if result.get('usage'): payload['usage'] = result['usage'] if result.get('skipped_reason'): payload['skipped_reason'] = result['skipped_reason'] return Response(payload) @action(detail=True, methods=['post']) def grade(self, request, pk=None): """ Выставить оценку. POST /api/homework/submissions/{id}/grade/ Body: { "score": 85, "feedback": "Отличная работа!" } """ submission = self.get_object() # Проверяем права if submission.homework.mentor != request.user: return Response( {'error': 'Только ментор может выставить оценку'}, status=status.HTTP_403_FORBIDDEN ) serializer = HomeworkGradeSerializer(submission, data=request.data, partial=True, context={'request': request}) serializer.is_valid(raise_exception=True) submission = serializer.save() # Инвалидируем кеш дашборда после оценки ДЗ from apps.users.cache_utils import invalidate_dashboard_cache invalidate_dashboard_cache(submission.student.id, 'client') invalidate_dashboard_cache(submission.homework.mentor.id, 'mentor') # Отправляем уведомление студенту from apps.notifications.services import NotificationService feedback_text = "" if submission.feedback: # Экранируем HTML теги в комментарии, чтобы не сломать разметку Telegram escaped_feedback = html.escape(submission.feedback) feedback_text = f"\n\n💬 Комментарий:\n{escaped_feedback}" NotificationService.create_notification_with_telegram( recipient=submission.student, notification_type='homework_reviewed', title='✅ ДЗ проверено', message=f'Проверено ДЗ "{submission.homework.title}". Оценка: {submission.score}/5{feedback_text}', priority='normal', action_url=f'/homework/{submission.homework.id}/submissions/{submission.id}/', content_object=submission ) response_serializer = HomeworkSubmissionSerializer(submission) return Response(response_serializer.data) @action(detail=False, methods=['get']) def by_subject(self, request): """ Получить ДЗ с оценками по выбранному предмету для графика прогресса. GET /api/homework/submissions/by_subject/ Query params: - subject: название предмета (обязательно) - start_date: начальная дата (YYYY-MM-DD, опционально) - end_date: конечная дата (YYYY-MM-DD, опционально) - child_id: ID ребенка (для родителей, опционально) Returns: список ДЗ с оценками, отсортированный по дате проверки """ from django.utils.dateparse import parse_date from django.db.models import Q from apps.schedule.models import Lesson user = request.user subject = request.query_params.get('subject') start_date = request.query_params.get('start_date') end_date = request.query_params.get('end_date') child_id = request.query_params.get('child_id') if not subject: return Response( {'error': 'Параметр subject обязателен'}, status=status.HTTP_400_BAD_REQUEST ) # Определяем для какого студента получаем ДЗ student_user = user if user.role == 'parent' and child_id: try: from apps.users.models import Client, Parent # Получаем профиль родителя parent_profile = Parent.objects.get(user=user) # child_id - это user_id ребенка, находим Client через User child_client = Client.objects.get(user_id=child_id) # Проверяем, что ребенок принадлежит этому родителю if child_client not in parent_profile.children.all(): return Response( {'error': 'Ребенок не найден'}, status=status.HTTP_404_NOT_FOUND ) student_user = child_client.user except (Client.DoesNotExist, Parent.DoesNotExist): return Response( {'error': 'Ребенок не найден'}, status=status.HTTP_404_NOT_FOUND ) elif user.role == 'mentor' and child_id: try: from apps.users.models import Client # child_id — user_id студента; ментор получает ДЗ своего ученика student_user = User.objects.get(id=child_id) child_client = Client.objects.get(user=student_user) # Проверяем, что у ментора есть занятия с этим студентом from apps.schedule.models import Lesson if not Lesson.objects.filter(mentor=user, client=child_client).exists(): return Response( {'error': 'Студент не найден'}, status=status.HTTP_404_NOT_FOUND ) except (User.DoesNotExist, Client.DoesNotExist): return Response( {'error': 'Студент не найден'}, status=status.HTTP_404_NOT_FOUND ) elif user.role != 'client': return Response( {'error': 'Доступ запрещен'}, status=status.HTTP_403_FORBIDDEN ) # Базовый queryset: только проверенные ДЗ с оценками для студента queryset = HomeworkSubmission.objects.filter( student=student_user, status='graded', score__isnull=False, checked_at__isnull=False ).select_related( 'homework', 'homework__lesson', 'homework__lesson__subject', 'homework__lesson__mentor_subject', 'homework__mentor', 'student' ) # Фильтр по предмету через урок # Проверяем и subject (ForeignKey) и subject_name (legacy поле) # Также проверяем, что урок существует (homework__lesson__isnull=False) # Используем OR для всех возможных вариантов названия предмета subject_filter = ( Q(homework__lesson__isnull=False) & ( Q(homework__lesson__subject__name__iexact=subject) | Q(homework__lesson__subject_name__iexact=subject) | Q(homework__lesson__mentor_subject__name__iexact=subject) ) ) # Также проверяем, если в сериализаторе homework есть поле lesson_subject # Но это поле вычисляемое, поэтому фильтруем только через урок # Логирование для отладки import logging logger = logging.getLogger(__name__) # Сначала получаем все ДЗ без фильтра по предмету для отладки debug_queryset = queryset.all() logger.info(f'[by_subject] Всего ДЗ для студента {student_user.id}: {debug_queryset.count()}') # Проверяем, есть ли ДЗ с уроками with_lessons = debug_queryset.filter(homework__lesson__isnull=False) logger.info(f'[by_subject] ДЗ с уроками: {with_lessons.count()}') # Проверяем предметы в уроках if with_lessons.exists(): sample = with_lessons.first() if sample.homework.lesson: lesson = sample.homework.lesson logger.info(f'[by_subject] Пример урока: id={lesson.id}, subject={lesson.subject}, subject_name={lesson.subject_name}, mentor_subject={lesson.mentor_subject}') if lesson.subject: logger.info(f'[by_subject] subject.name={lesson.subject.name}') if lesson.mentor_subject: logger.info(f'[by_subject] mentor_subject.name={lesson.mentor_subject.name}') queryset = queryset.filter(subject_filter) logger.info(f'[by_subject] После фильтра по предмету "{subject}": {queryset.count()}') # Фильтр по дате проверки if start_date: try: start = parse_date(start_date) if start: queryset = queryset.filter(checked_at__date__gte=start) except (ValueError, TypeError): pass if end_date: try: end = parse_date(end_date) if end: queryset = queryset.filter(checked_at__date__lte=end) except (ValueError, TypeError): pass # Сортируем по дате проверки queryset = queryset.order_by('checked_at') # Сериализуем только необходимые поля для графика serializer = HomeworkSubmissionSerializer(queryset, many=True) return Response({ 'count': queryset.count(), 'results': serializer.data }) @action(detail=True, methods=['post']) def return_for_revision(self, request, pk=None): """ Вернуть на доработку. POST /api/homework/submissions/{id}/return_for_revision/ Body: { "feedback": "Необходимо доработать..." } """ submission = self.get_object() # Проверяем права (только ментор задания) if submission.homework.mentor != request.user: return Response( {'error': 'Только ментор может вернуть на доработку'}, status=status.HTTP_403_FORBIDDEN ) serializer = self.get_serializer(submission, data=request.data) serializer.is_valid(raise_exception=True) submission = serializer.save() # Отправляем уведомление студенту from apps.notifications.services import NotificationService msg = f'ДЗ "{submission.homework.title}" возвращено на доработку. Нужно отправить решение заново.' if submission.feedback and str(submission.feedback).strip(): comment = (submission.feedback[:500] + '…') if len(submission.feedback) > 500 else submission.feedback msg += f'\n\n💬 Комментарий:\n{comment}' NotificationService.create_notification_with_telegram( recipient=submission.student, notification_type='homework_returned', title='🔄 ДЗ возвращено на доработку', message=msg, priority='normal', action_url=f'/homework/{submission.homework.id}/submissions/{submission.id}/', content_object=submission ) response_serializer = HomeworkSubmissionSerializer(submission) return Response(response_serializer.data) @action(detail=False, methods=['get']) def my_submissions(self, request): """ Мои решения (для студентов). GET /api/homework/submissions/my_submissions/ """ submissions = HomeworkSubmission.objects.filter( student=request.user ).select_related( 'homework', 'homework__mentor', 'homework__lesson', 'homework__lesson__subject', 'homework__lesson__mentor_subject', 'student', 'checked_by' ).only( 'id', 'homework_id', 'student_id', 'checked_by_id', 'content', 'score', 'feedback', 'status', 'submitted_at', 'checked_at', 'is_late', 'passed', 'updated_at' ) serializer = HomeworkSubmissionSerializer(submissions, many=True) return Response(serializer.data) @action(detail=False, methods=['get']) def pending(self, request): """ Решения ожидающие проверки (для менторов). GET /api/homework/submissions/pending/ """ submissions = HomeworkSubmission.objects.filter( homework__mentor=request.user, status='pending' ).select_related('homework', 'homework__mentor', 'student', 'checked_by').only( 'id', 'homework_id', 'student_id', 'checked_by_id', 'content', 'score', 'feedback', 'status', 'submitted_at', 'checked_at', 'is_late', 'passed', 'updated_at' ) serializer = HomeworkSubmissionSerializer(submissions, many=True) return Response(serializer.data) class HomeworkFileViewSet(viewsets.ModelViewSet): """ ViewSet для управления файлами ДЗ. """ permission_classes = [IsAuthenticated] serializer_class = HomeworkFileSerializer def get_queryset(self): """Получение файлов.""" user = self.request.user queryset = HomeworkFile.objects.filter( models.Q(homework__mentor=user) | models.Q(submission__student=user) | models.Q(uploaded_by=user) ).select_related('homework', 'homework__mentor', 'submission', 'submission__student', 'uploaded_by') # Оптимизация: для списка используем only() для ограничения полей if self.action == 'list': queryset = queryset.only( 'id', 'homework_id', 'submission_id', 'file_type', 'file', 'filename', 'file_size', 'uploaded_by_id', 'created_at' ) return queryset.order_by('-created_at') def perform_create(self, serializer): """Создание файла.""" serializer.save(uploaded_by=self.request.user) class HomeworkAIAgentViewSet(viewsets.ReadOnlyModelViewSet): """ Список ИИ-агентов для проверки ДЗ (только чтение). GET /api/homework/ai-agents/ — какие у нас есть ИИ-агенты. """ permission_classes = [IsAuthenticated] serializer_class = HomeworkAIAgentSerializer queryset = HomeworkAIAgent.objects.filter(is_active=True).order_by('order', 'name')