262 lines
12 KiB
Python
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,
|
|
})
|
|
|