""" API views для прогресса студентов. """ 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.models import Q, Count, Value, Case, When, F, CharField from django.utils import timezone from datetime import timedelta, datetime from apps.users.models import User, Client from apps.schedule.models import Lesson from apps.homework.models import HomeworkSubmission, Homework from apps.users.utils import format_datetime_for_user def _lesson_display_subject(lesson): """Название предмета для занятия: subject_name, иначе subject, иначе mentor_subject.""" if lesson.subject_name and lesson.subject_name.strip(): return lesson.subject_name.strip() if lesson.subject_id and lesson.subject: return lesson.subject.name if lesson.mentor_subject_id and lesson.mentor_subject: return lesson.mentor_subject.name return 'Другое' class StudentProgressViewSet(viewsets.ViewSet): """ViewSet для прогресса студентов.""" permission_classes = [IsAuthenticated] @action(detail=False, methods=['get'], url_path='(?P[^/.]+)/progress') def student_progress(self, request, student_id=None): """ Получить детальный прогресс студента по предметам. GET /api/student-progress/{student_id}/progress/ student_id - это ID клиента (Client.id), не User.id """ try: # student_id это Client ID, находим User через Client client = Client.objects.select_related('user').get(id=student_id) student = client.user except Client.DoesNotExist: return Response( {'error': 'Студент не найден'}, status=status.HTTP_404_NOT_FOUND ) # Проверяем что это студент текущего ментора # (пропускаем проверку, так как структура модели может отличаться) # Фильтры из query-параметров: subject, start_date, end_date subject_filter = request.query_params.get('subject', '').strip() start_date_str = request.query_params.get('start_date', '').strip() end_date_str = request.query_params.get('end_date', '').strip() start_date = None end_date = None if start_date_str: try: start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date() except ValueError: pass if end_date_str: try: end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date() except ValueError: pass # База занятий: ментор + студент + период (без фильтра по предмету) # Список предметов в ответе всегда полный — для выпадающего списка на фронте. lessons_base = Lesson.objects.filter( Q(client=client) | Q(group__isnull=False), mentor=request.user ).select_related('subject', 'mentor_subject').annotate( display_subject=Case( When(subject_name__gt='', then=F('subject_name')), When(subject__isnull=False, then=F('subject__name')), When(mentor_subject__isnull=False, then=F('mentor_subject__name')), default=Value('Другое'), output_field=CharField(), ) ) if start_date: lessons_base = lessons_base.filter(start_time__date__gte=start_date) if end_date: lessons_base = lessons_base.filter(start_time__date__lte=end_date) # Для графика по дням — фильтр по предмету (если передан). lessons_filtered = lessons_base if subject_filter: lessons_filtered = lessons_filtered.filter(display_subject=subject_filter) # Делаем порядок предметов стабильным (важно для "первого в селекте" на фронте) lessons_by_subject = lessons_base.values('display_subject').annotate( total_lessons=Count('id'), completed_lessons=Count('id', filter=Q(status='completed')), ).order_by('display_subject') subjects_stats = {} for item in lessons_by_subject: name = item['display_subject'] or 'Другое' subjects_stats[name] = { 'subject': name, 'total_lessons': item['total_lessons'], 'completed_lessons': item['completed_lessons'], 'grades': [], 'average_grade': 0, 'homework_count': 0, 'homework_completed': 0, 'homework_average': 0, } completed_lessons = lessons_base.filter(status='completed') for lesson in completed_lessons: grade = None if hasattr(lesson, 'mentor_grade') and lesson.mentor_grade: grade = float(lesson.mentor_grade) elif hasattr(lesson, 'school_grade') and lesson.school_grade: grade = float(lesson.school_grade) if not grade: continue name = lesson.display_subject or 'Другое' if name not in subjects_stats: subjects_stats[name] = { 'subject': name, 'total_lessons': 0, 'completed_lessons': 0, 'grades': [], 'average_grade': 0, 'homework_count': 0, 'homework_completed': 0, 'homework_average': 0, } subjects_stats[name]['grades'].append({ 'grade': grade, 'date': format_datetime_for_user(lesson.start_time, request.user.timezone) if lesson.start_time else '', 'comment': lesson.title or '', }) lesson_ids = list(lessons_base.values_list('id', flat=True)) submissions_qs = HomeworkSubmission.objects.filter( student=student, status='graded', homework__lesson_id__in=lesson_ids ).select_related( 'homework', 'homework__lesson', 'homework__lesson__subject', 'homework__lesson__mentor_subject' ).only( 'id', 'homework_id', 'student_id', 'score', 'passed', 'submitted_at', 'updated_at' ) submissions = list(submissions_qs) for sub in submissions: lesson = sub.homework.lesson if sub.homework else None if not lesson: continue name = _lesson_display_subject(lesson) if name not in subjects_stats: subjects_stats[name] = { 'subject': name, 'total_lessons': 0, 'completed_lessons': 0, 'grades': [], 'average_grade': 0, 'homework_count': 0, 'homework_completed': 0, 'homework_average': 0, } subjects_stats[name]['homework_count'] = subjects_stats[name].get('homework_count', 0) + 1 if sub.passed: subjects_stats[name]['homework_completed'] = subjects_stats[name].get('homework_completed', 0) + 1 if sub.score is not None: subjects_stats[name]['grades'].append({ 'grade': float(sub.score), 'date': format_datetime_for_user(sub.submitted_at or sub.updated_at, request.user.timezone) if (sub.submitted_at or sub.updated_at) else None, 'comment': f'ДЗ: {sub.homework.title}', }) for subject, stats in subjects_stats.items(): if stats['grades']: stats['average_grade'] = sum(g['grade'] for g in stats['grades']) / len(stats['grades']) stats['grades'].sort(key=lambda x: x['date'] or '') if stats['homework_count'] > 0: stats['homework_average'] = (stats['homework_completed'] / stats['homework_count']) * 100 all_grades = [] for stats in subjects_stats.values(): all_grades.extend(stats['grades']) overall_average = sum(g['grade'] for g in all_grades) / len(all_grades) if all_grades else 0 daily_stats = [] if start_date and end_date and start_date <= end_date: daily_agg = lessons_filtered.values('start_time__date').annotate( total_lessons=Count('id'), completed_lessons=Count('id', filter=Q(status='completed')), ) by_date = { row['start_time__date']: { 'total_lessons': row['total_lessons'], 'completed_lessons': row['completed_lessons'], } for row in daily_agg if row.get('start_time__date') } current = start_date while current <= end_date: e = by_date.get(current, {'total_lessons': 0, 'completed_lessons': 0}) daily_stats.append({ 'date': current.isoformat(), 'total_lessons': e['total_lessons'], 'completed_lessons': e['completed_lessons'], }) current += timedelta(days=1) if start_date and end_date: recent_submissions = [ s for s in submissions if s.submitted_at and start_date <= s.submitted_at.date() <= end_date ] recent_submissions.sort(key=lambda s: s.submitted_at or s.updated_at) else: thirty_days_ago = timezone.now() - timedelta(days=30) recent_submissions = [ s for s in submissions if (s.submitted_at or s.updated_at) and (s.submitted_at or s.updated_at) >= thirty_days_ago ] recent_submissions.sort(key=lambda s: s.submitted_at or s.updated_at) progress_timeline = [] for sub in recent_submissions: if sub.score is None: continue lesson = sub.homework.lesson if sub.homework else None subject_name = _lesson_display_subject(lesson) if lesson else 'Другое' progress_timeline.append({ 'date': format_datetime_for_user(sub.submitted_at or sub.updated_at, request.user.timezone) if (sub.submitted_at or sub.updated_at) else None, 'grade': float(sub.score), 'subject': subject_name, 'comment': f'ДЗ: {sub.homework.title}', }) return Response({ 'student': { 'id': student.id, 'name': f'{student.first_name} {student.last_name}', 'email': student.email, }, 'overall': { 'average_grade': round(overall_average, 2), 'total_lessons': sum(s['total_lessons'] for s in subjects_stats.values()), 'completed_lessons': sum(s['completed_lessons'] for s in subjects_stats.values()), 'total_grades': len(all_grades), }, 'subjects': list(subjects_stats.values()), 'progress_timeline': progress_timeline, 'daily_stats': daily_stats, })