uchill/backend/apps/users/student_progress_views.py

262 lines
12 KiB
Python

"""
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<student_id>[^/.]+)/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,
})