1256 lines
55 KiB
Python
1256 lines
55 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 import models
|
||
from django.utils import timezone
|
||
from django.core.cache import cache
|
||
from datetime import timedelta
|
||
import calendar
|
||
import logging
|
||
from collections import defaultdict
|
||
from .utils import format_datetime_for_user
|
||
from .models import User, Client, Parent
|
||
from apps.schedule.models import Lesson
|
||
from apps.homework.models import Homework, HomeworkSubmission
|
||
from apps.materials.models import Material
|
||
from apps.notifications.models import Notification
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class MentorDashboardViewSet(viewsets.ViewSet):
|
||
"""
|
||
ViewSet для дашборда ментора.
|
||
|
||
dashboard: Основная статистика
|
||
clients: Список клиентов
|
||
upcoming_lessons: Предстоящие занятия
|
||
statistics: Детальная статистика
|
||
recent_activity: Недавняя активность
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
@action(detail=False, methods=['get'])
|
||
def dashboard(self, request):
|
||
"""
|
||
Основной дашборд ментора.
|
||
|
||
GET /api/users/mentor/dashboard/
|
||
"""
|
||
user = request.user
|
||
|
||
if user.role != 'mentor':
|
||
return Response(
|
||
{'error': 'Только для менторов'},
|
||
status=status.HTTP_403_FORBIDDEN
|
||
)
|
||
|
||
# Кеширование: кеш на 30 секунд для каждого пользователя (для актуальности уведомлений)
|
||
cache_key = f'mentor_dashboard_{user.id}'
|
||
cached_data = cache.get(cache_key)
|
||
|
||
if cached_data is not None:
|
||
return Response(cached_data)
|
||
|
||
# Временные рамки
|
||
now = timezone.now()
|
||
week_ago = now - timedelta(days=7)
|
||
# Текущий календарный месяц (с 1 по последний день месяца)
|
||
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||
# Конец текущего месяца (последний день месяца, 23:59:59.999999)
|
||
last_day = calendar.monthrange(now.year, now.month)[1]
|
||
month_end = now.replace(day=last_day, hour=23, minute=59, second=59, microsecond=999999)
|
||
|
||
# Клиенты - один запрос
|
||
clients = Client.objects.filter(mentors=user).select_related('user').prefetch_related('mentors')
|
||
total_clients = clients.count()
|
||
|
||
# Занятия - оптимизация: используем aggregate для всех подсчетов
|
||
from django.db.models import Count, Sum, Q
|
||
lessons = Lesson.objects.filter(mentor=user.id).select_related(
|
||
'mentor', 'client', 'client__user', 'subject', 'mentor_subject'
|
||
)
|
||
|
||
# Один запрос для всех подсчетов занятий
|
||
lessons_stats = lessons.aggregate(
|
||
total=Count('id'),
|
||
this_week=Count('id', filter=Q(start_time__gte=week_ago)),
|
||
this_month=Count('id', filter=Q(start_time__gte=month_start) & Q(start_time__lte=month_end)),
|
||
completed=Count('id', filter=Q(status='completed'))
|
||
)
|
||
|
||
total_lessons = lessons_stats['total']
|
||
lessons_this_week = lessons_stats['this_week']
|
||
lessons_this_month = lessons_stats['this_month']
|
||
completed_lessons = lessons_stats['completed']
|
||
|
||
# Ближайшие занятия
|
||
upcoming_lessons = lessons.filter(
|
||
start_time__gte=now,
|
||
status__in=['scheduled', 'in_progress']
|
||
).select_related('client', 'client__user', 'subject', 'mentor_subject').order_by('start_time')[:5]
|
||
|
||
# Домашние задания - один запрос
|
||
homeworks = Homework.objects.filter(mentor=user.id).select_related('mentor', 'lesson')
|
||
total_homeworks = homeworks.count()
|
||
|
||
# Отправки ДЗ - один запрос
|
||
pending_submissions = HomeworkSubmission.objects.filter(
|
||
homework__mentor=user,
|
||
status='pending'
|
||
).select_related('homework', 'student').count()
|
||
|
||
# Последние сданные ДЗ (4 шт)
|
||
recent_submissions = HomeworkSubmission.objects.filter(
|
||
homework__mentor=user,
|
||
status__in=['submitted', 'graded', 'returned']
|
||
).select_related('homework', 'homework__lesson', 'homework__lesson__subject', 'homework__lesson__mentor_subject', 'student').order_by('-submitted_at')[:4]
|
||
|
||
# Материалы - один запрос
|
||
materials = Material.objects.filter(owner=user, is_deleted=False).select_related('owner')
|
||
total_materials = materials.count()
|
||
|
||
# Уведомления - один запрос (только in_app уведомления)
|
||
from apps.notifications.models import Notification
|
||
unread_notifications = Notification.objects.filter(
|
||
recipient=user,
|
||
is_read=False,
|
||
channel='in_app' # Только in_app уведомления
|
||
).select_related('recipient').count()
|
||
|
||
# Доходы - оптимизация: один запрос для обоих подсчетов
|
||
revenue_stats = lessons.filter(
|
||
status='completed'
|
||
).exclude(price__isnull=True).exclude(price=0).aggregate(
|
||
total=Sum('price'),
|
||
this_month=Sum('price', filter=Q(start_time__gte=month_start))
|
||
)
|
||
total_revenue = revenue_stats['total'] or 0
|
||
revenue_this_month = revenue_stats['this_month'] or 0
|
||
|
||
response_data = {
|
||
'summary': {
|
||
'total_clients': total_clients,
|
||
'total_lessons': total_lessons,
|
||
'lessons_this_week': lessons_this_week,
|
||
'lessons_this_month': lessons_this_month,
|
||
'completed_lessons': completed_lessons,
|
||
'total_homeworks': total_homeworks,
|
||
'pending_submissions': pending_submissions,
|
||
'total_materials': total_materials,
|
||
'unread_notifications': unread_notifications,
|
||
'total_revenue': float(total_revenue),
|
||
'revenue_this_month': float(revenue_this_month)
|
||
},
|
||
'upcoming_lessons': [
|
||
{
|
||
'id': str(lesson.id),
|
||
'title': lesson.title,
|
||
'subject': (
|
||
lesson.subject.name if lesson.subject
|
||
else lesson.mentor_subject.name if lesson.mentor_subject
|
||
else lesson.subject_name if lesson.subject_name
|
||
else ''
|
||
),
|
||
'client': {
|
||
'id': str(lesson.client.id),
|
||
'name': lesson.client.user.get_full_name() if lesson.client.user else 'Студент',
|
||
'avatar': request.build_absolute_uri(lesson.client.user.avatar.url) if lesson.client.user and lesson.client.user.avatar else None,
|
||
'first_name': lesson.client.user.first_name if lesson.client.user else '',
|
||
'last_name': lesson.client.user.last_name if lesson.client.user else ''
|
||
},
|
||
'start_time': format_datetime_for_user(lesson.start_time, request.user.timezone) if lesson.start_time else None,
|
||
'end_time': format_datetime_for_user(lesson.end_time, request.user.timezone) if lesson.end_time else None
|
||
}
|
||
for lesson in upcoming_lessons
|
||
],
|
||
'recent_submissions': [
|
||
{
|
||
'id': str(submission.id),
|
||
'homework': {
|
||
'id': str(submission.homework.id),
|
||
'title': submission.homework.title,
|
||
},
|
||
'subject': (
|
||
submission.homework.lesson.subject.name if submission.homework.lesson and submission.homework.lesson.subject
|
||
else submission.homework.lesson.mentor_subject.name if submission.homework.lesson and submission.homework.lesson.mentor_subject
|
||
else submission.homework.lesson.subject_name if submission.homework.lesson and submission.homework.lesson.subject_name
|
||
else ''
|
||
),
|
||
'student': {
|
||
'id': str(submission.student.id),
|
||
'name': submission.student.get_full_name() if submission.student else 'Студент',
|
||
'avatar': request.build_absolute_uri(submission.student.avatar.url) if submission.student and submission.student.avatar else None,
|
||
'first_name': submission.student.first_name if submission.student else '',
|
||
'last_name': submission.student.last_name if submission.student else ''
|
||
},
|
||
'status': submission.status,
|
||
'score': submission.score,
|
||
'max_score': submission.homework.max_score,
|
||
'submitted_at': format_datetime_for_user(submission.submitted_at, request.user.timezone) if submission.submitted_at else None,
|
||
}
|
||
for submission in recent_submissions
|
||
]
|
||
}
|
||
|
||
# Сохраняем в кеш на 30 секунд для актуальности уведомлений
|
||
cache.set(cache_key, response_data, 30)
|
||
|
||
return Response(response_data)
|
||
|
||
@action(detail=False, methods=['get'])
|
||
def clients(self, request):
|
||
"""
|
||
Список клиентов ментора.
|
||
|
||
GET /api/users/mentor/clients/
|
||
"""
|
||
user = request.user
|
||
|
||
if user.role != 'mentor':
|
||
return Response(
|
||
{'error': 'Только для менторов'},
|
||
status=status.HTTP_403_FORBIDDEN
|
||
)
|
||
|
||
clients = Client.objects.filter(mentors=user).select_related('user').prefetch_related('mentors')
|
||
|
||
# Оптимизация: получаем все данные одним batch-запросом
|
||
client_ids = [client.id for client in clients]
|
||
client_user_ids = [client.user.id for client in clients]
|
||
|
||
# Оптимизация: один запрос для всех статистик занятий всех клиентов
|
||
from django.db.models import Count, Avg, Q
|
||
now = timezone.now()
|
||
# Исправление: используем client__user_id для фильтрации по пользователям клиентов
|
||
lessons_stats = Lesson.objects.filter(
|
||
client__user_id__in=client_user_ids,
|
||
mentor=user
|
||
).values('client__user_id').annotate(
|
||
total=Count('id'),
|
||
completed=Count('id', filter=Q(status='completed')),
|
||
upcoming=Count('id', filter=Q(start_time__gte=now, status='confirmed'))
|
||
)
|
||
lessons_by_client = {item['client__user_id']: item for item in lessons_stats}
|
||
|
||
# Оптимизация: один запрос для всех статистик ДЗ всех клиентов
|
||
submissions_stats = HomeworkSubmission.objects.filter(
|
||
homework__mentor=user,
|
||
student_id__in=client_user_ids
|
||
).values('student_id').annotate(
|
||
total=Count('id'),
|
||
completed=Count('id', filter=Q(status='graded', passed=True)),
|
||
avg_score=Avg('score', filter=Q(status='graded'))
|
||
)
|
||
submissions_by_client = {item['student_id']: item for item in submissions_stats}
|
||
|
||
# Оптимизация: один запрос для последних занятий всех клиентов
|
||
from django.db.models import Max
|
||
last_lessons = Lesson.objects.filter(
|
||
client__user_id__in=client_user_ids,
|
||
mentor=user
|
||
).values('client__user_id').annotate(
|
||
last_lesson_time=Max('start_time')
|
||
)
|
||
last_lessons_by_client = {item['client__user_id']: item['last_lesson_time'] for item in last_lessons}
|
||
|
||
clients_data = []
|
||
for client in clients:
|
||
# Получаем статистику из предзагруженных данных
|
||
lesson_stats = lessons_by_client.get(client.user.id, {'total': 0, 'completed': 0, 'upcoming': 0})
|
||
submission_stats = submissions_by_client.get(client.user.id, {'total': 0, 'completed': 0, 'avg_score': 0})
|
||
|
||
total_lessons = lesson_stats['total']
|
||
completed_lessons = lesson_stats['completed']
|
||
upcoming_lessons = lesson_stats['upcoming']
|
||
|
||
total_homeworks = submission_stats['total']
|
||
completed_homeworks = submission_stats['completed']
|
||
average_score = submission_stats['avg_score'] or 0
|
||
|
||
clients_data.append({
|
||
'id': client.id,
|
||
'user': {
|
||
'id': client.user.id,
|
||
'email': client.user.email,
|
||
'first_name': client.user.first_name,
|
||
'last_name': client.user.last_name,
|
||
'phone': client.user.phone,
|
||
'avatar': client.user.avatar.url if client.user.avatar else None
|
||
},
|
||
'statistics': {
|
||
'total_lessons': total_lessons,
|
||
'completed_lessons': completed_lessons,
|
||
'upcoming_lessons': upcoming_lessons,
|
||
'total_homeworks': total_homeworks,
|
||
'completed_homeworks': completed_homeworks,
|
||
'average_score': round(average_score, 2)
|
||
},
|
||
'last_lesson': last_lessons_by_client.get(client.user.id)
|
||
})
|
||
|
||
return Response(clients_data)
|
||
|
||
@action(detail=False, methods=['get'])
|
||
def statistics(self, request):
|
||
"""
|
||
Детальная статистика ментора.
|
||
|
||
GET /api/users/mentor/statistics/
|
||
"""
|
||
user = request.user
|
||
|
||
if user.role != 'mentor':
|
||
return Response(
|
||
{'error': 'Только для менторов'},
|
||
status=status.HTTP_403_FORBIDDEN
|
||
)
|
||
|
||
# Период статистики
|
||
period = request.query_params.get('period', '30') # дни
|
||
days = int(period)
|
||
start_date = timezone.now() - timedelta(days=days)
|
||
|
||
# Оптимизация: используем aggregate для всех подсчетов одним запросом
|
||
lessons = Lesson.objects.filter(
|
||
mentor=user,
|
||
start_time__gte=start_date
|
||
)
|
||
|
||
# Оптимизация: получаем все статистики одним запросом
|
||
lessons_stats = lessons.aggregate(
|
||
total=models.Count('id')
|
||
)
|
||
|
||
# Занятия по дням
|
||
lessons_by_day = lessons.extra(
|
||
select={'day': 'DATE(start_time)'}
|
||
).values('day').annotate(
|
||
count=models.Count('id')
|
||
).order_by('day')
|
||
|
||
# Занятия по статусам
|
||
lessons_by_status = lessons.values('status').annotate(
|
||
count=models.Count('id')
|
||
)
|
||
|
||
# Оптимизация: один запрос для всех статистик ДЗ
|
||
from django.db.models import Q
|
||
homeworks = Homework.objects.filter(
|
||
mentor=user,
|
||
created_at__gte=start_date
|
||
)
|
||
|
||
submissions = HomeworkSubmission.objects.filter(
|
||
homework__mentor=user,
|
||
submitted_at__gte=start_date
|
||
)
|
||
|
||
# Оптимизация: один запрос для всех статистик submissions
|
||
submissions_stats = submissions.aggregate(
|
||
total=models.Count('id'),
|
||
avg_score=models.Avg('score', filter=Q(status='graded'))
|
||
)
|
||
|
||
# Оценки
|
||
grades_distribution = submissions.filter(
|
||
status='graded'
|
||
).values('score').annotate(
|
||
count=models.Count('id')
|
||
).order_by('score')
|
||
|
||
# Оптимизация: один запрос для всех статистик материалов
|
||
materials = Material.objects.filter(
|
||
owner=user,
|
||
is_deleted=False,
|
||
created_at__gte=start_date
|
||
)
|
||
|
||
materials_stats = materials.aggregate(
|
||
total=models.Count('id'),
|
||
total_views=models.Sum('views_count')
|
||
)
|
||
|
||
materials_by_type = materials.values('material_type').annotate(
|
||
count=models.Count('id')
|
||
)
|
||
|
||
return Response({
|
||
'period_days': days,
|
||
'lessons': {
|
||
'total': lessons_stats['total'],
|
||
'by_day': list(lessons_by_day),
|
||
'by_status': list(lessons_by_status)
|
||
},
|
||
'homeworks': {
|
||
'created': homeworks.count(),
|
||
'submissions': submissions_stats['total'],
|
||
'average_score': submissions_stats['avg_score'] or 0,
|
||
'grades_distribution': list(grades_distribution)
|
||
},
|
||
'materials': {
|
||
'total': materials_stats['total'],
|
||
'by_type': list(materials_by_type),
|
||
'total_views': materials_stats['total_views'] or 0
|
||
}
|
||
})
|
||
|
||
@action(detail=False, methods=['get'])
|
||
def income(self, request):
|
||
"""
|
||
Статистика доходов ментора.
|
||
|
||
GET /api/users/mentor/income/?period=day|week|month|range&start_date=2024-01-01&end_date=2024-01-31
|
||
"""
|
||
from django.db.models import Sum, Count
|
||
from datetime import datetime, date
|
||
|
||
user = request.user
|
||
|
||
if user.role != 'mentor':
|
||
return Response(
|
||
{'error': 'Только для менторов'},
|
||
status=status.HTTP_403_FORBIDDEN
|
||
)
|
||
|
||
# Получаем параметры
|
||
period = request.query_params.get('period', 'week') # day, week, month, range
|
||
start_date_str = request.query_params.get('start_date')
|
||
end_date_str = request.query_params.get('end_date')
|
||
|
||
# Используем локальное время сервера
|
||
now = timezone.now()
|
||
|
||
# Определяем диапазон дат для предустановленных периодов
|
||
if period == 'day':
|
||
# Текущий календарный день (с 00:00 до 23:59:59)
|
||
start_date = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||
end_date = start_date + timedelta(days=1) - timedelta(microseconds=1)
|
||
elif period == 'week':
|
||
# Текущая календарная неделя (с понедельника по воскресенье)
|
||
# weekday(): 0 = понедельник, 6 = воскресенье
|
||
monday = now - timedelta(days=now.weekday())
|
||
start_date = monday.replace(hour=0, minute=0, second=0, microsecond=0)
|
||
end_date = start_date + timedelta(days=7) - timedelta(microseconds=1)
|
||
elif period == 'month':
|
||
# Текущий календарный месяц: с 1-го числа до последнего дня месяца
|
||
start_date = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||
# Первый день следующего месяца
|
||
if start_date.month == 12:
|
||
next_month = start_date.replace(year=start_date.year + 1, month=1, day=1)
|
||
else:
|
||
next_month = start_date.replace(month=start_date.month + 1, day=1)
|
||
end_date = next_month - timedelta(microseconds=1)
|
||
elif period == 'range' and start_date_str and end_date_str:
|
||
try:
|
||
start_date = timezone.make_aware(datetime.strptime(start_date_str, '%Y-%m-%d'))
|
||
end_date = timezone.make_aware(datetime.strptime(end_date_str, '%Y-%m-%d'))
|
||
end_date = end_date.replace(hour=23, minute=59, second=59)
|
||
except ValueError:
|
||
return Response(
|
||
{'error': 'Неверный формат даты. Используйте YYYY-MM-DD'},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
else:
|
||
# По умолчанию - последние 90 дней
|
||
start_date = now - timedelta(days=90)
|
||
end_date = now
|
||
|
||
# Получаем завершенные занятия с ценой
|
||
# Сначала получаем все завершенные занятия для отладки
|
||
all_completed = Lesson.objects.filter(
|
||
mentor=user,
|
||
status='completed'
|
||
)
|
||
|
||
# Проверяем занятия без цены
|
||
without_price = all_completed.filter(price__isnull=True) | all_completed.filter(price=0)
|
||
|
||
# Логируем для отладки
|
||
import logging
|
||
logger = logging.getLogger(__name__)
|
||
logger.info(f'[Income] All completed lessons: {all_completed.count()}')
|
||
logger.info(f'[Income] Lessons without price: {without_price.count()}')
|
||
logger.info(f'[Income] Date range: {start_date} to {end_date}')
|
||
|
||
# Проверяем занятия с ценой и без цены
|
||
with_price = all_completed.exclude(price__isnull=True).exclude(price=0)
|
||
logger.info(f'[Income] Lessons with price: {with_price.count()}')
|
||
|
||
# Фильтруем по датам и цене для всех периодов
|
||
lessons = with_price.filter(
|
||
start_time__gte=start_date,
|
||
start_time__lte=end_date
|
||
)
|
||
logger.info(f'[Income] Using {period} period - filtered lessons: {lessons.count()}')
|
||
|
||
logger.info(f'[Income] Filtered lessons: {lessons.count()}')
|
||
if lessons.exists():
|
||
sample_lessons = list(lessons[:5])
|
||
logger.info(f'[Income] Sample lessons: {[{"id": l.id, "price": float(l.price) if l.price else 0, "start_time": str(l.start_time)} for l in sample_lessons]}')
|
||
|
||
# Оптимизация: получаем все статистики одним запросом
|
||
lessons_stats = lessons.aggregate(
|
||
total_income=Sum('price'),
|
||
total_lessons=Count('id')
|
||
)
|
||
total_income = lessons_stats['total_income'] or 0
|
||
total_lessons = lessons_stats['total_lessons']
|
||
average_lesson_price = total_income / total_lessons if total_lessons > 0 else 0
|
||
|
||
logger.info(f'[Income] Total income: {total_income}, Total lessons: {total_lessons}, Avg price: {average_lesson_price}')
|
||
|
||
# Группировка по дням
|
||
if period == 'day':
|
||
# По часам для дня
|
||
# Оптимизация: используем один запрос с ExtractHour вместо 24 запросов
|
||
from django.db.models.functions import ExtractHour
|
||
by_hour_data = lessons.annotate(
|
||
hour=ExtractHour('start_time')
|
||
).values('hour').annotate(
|
||
total=Sum('price'),
|
||
count=Count('id')
|
||
).order_by('hour')
|
||
|
||
# Создаем словарь для быстрого доступа
|
||
by_hour_dict = {item['hour']: item for item in by_hour_data}
|
||
|
||
# Заполняем все 24 часа (если для какого-то часа нет данных, значения = 0)
|
||
chart_data = []
|
||
for hour in range(24):
|
||
hour_start = start_date.replace(hour=hour, minute=0, second=0, microsecond=0)
|
||
hour_stats = by_hour_dict.get(hour, {'total': 0, 'count': 0})
|
||
chart_data.append({
|
||
'date': hour_start.strftime('%H:00'),
|
||
'income': float(hour_stats['total'] or 0),
|
||
'lessons': hour_stats['count']
|
||
})
|
||
else:
|
||
# По дням для недели/месяца/диапазона
|
||
chart_data = []
|
||
current_date = start_date.date()
|
||
end_date_only = end_date.date()
|
||
|
||
while current_date <= end_date_only:
|
||
day_start = timezone.make_aware(datetime.combine(current_date, datetime.min.time()))
|
||
day_end = day_start + timedelta(days=1)
|
||
|
||
day_lessons = lessons.filter(
|
||
start_time__gte=day_start,
|
||
start_time__lt=day_end
|
||
)
|
||
# Оптимизация: один запрос для статистики дня
|
||
day_stats = day_lessons.aggregate(
|
||
total=Sum('price'),
|
||
count=Count('id')
|
||
)
|
||
|
||
chart_data.append({
|
||
'date': current_date.strftime('%d.%m'),
|
||
'income': float(day_stats['total'] or 0),
|
||
'lessons': day_stats['count']
|
||
})
|
||
|
||
current_date += timedelta(days=1)
|
||
|
||
# Статистика по занятиям (топ занятий по доходам)
|
||
lessons_stats = lessons.values(
|
||
'title',
|
||
'subject',
|
||
'client__user__first_name',
|
||
'client__user__last_name',
|
||
'group__id',
|
||
'group__name',
|
||
).annotate(
|
||
total_income=Sum('price'),
|
||
lessons_count=Count('id'),
|
||
).order_by('-total_income')[:10]
|
||
|
||
return Response({
|
||
'period': period,
|
||
'start_date': format_datetime_for_user(start_date, request.user.timezone) if start_date else None,
|
||
'end_date': format_datetime_for_user(end_date, request.user.timezone) if end_date else None,
|
||
'summary': {
|
||
'total_income': float(total_income),
|
||
'total_lessons': total_lessons,
|
||
'average_lesson_price': float(average_lesson_price)
|
||
},
|
||
'chart_data': chart_data,
|
||
'top_lessons': [
|
||
{
|
||
'lesson_title': item['title'] or 'Без названия',
|
||
'subject': item['subject'] or '',
|
||
'is_group': bool(item['group__id']),
|
||
'target_name': (
|
||
item['group__name']
|
||
if item['group__name']
|
||
else f"{item['client__user__first_name']} {item['client__user__last_name']}".strip() or 'Ученик'
|
||
),
|
||
'lessons_count': item['lessons_count'],
|
||
'total_income': float(item['total_income']),
|
||
}
|
||
for item in lessons_stats
|
||
],
|
||
})
|
||
|
||
|
||
class ClientDashboardViewSet(viewsets.ViewSet):
|
||
"""
|
||
ViewSet для дашборда клиента.
|
||
|
||
dashboard: Основная статистика
|
||
my_lessons: Мои занятия
|
||
my_homeworks: Мои домашние задания
|
||
progress: Прогресс обучения
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
@action(detail=False, methods=['get'])
|
||
def dashboard(self, request):
|
||
"""
|
||
Основной дашборд клиента.
|
||
|
||
GET /api/users/client/dashboard/
|
||
"""
|
||
user = request.user
|
||
|
||
# Кеширование: кеш на 2 минуты для каждого пользователя
|
||
cache_key = f'client_dashboard_{user.id}'
|
||
cached_data = cache.get(cache_key)
|
||
|
||
if cached_data is not None:
|
||
return Response(cached_data)
|
||
|
||
# Временные рамки
|
||
now = timezone.now()
|
||
week_ago = now - timedelta(days=7)
|
||
month_ago = now - timedelta(days=30)
|
||
|
||
# Занятия - оптимизация: используем select_related и aggregate
|
||
from django.db.models import Count, Avg, Q
|
||
lessons = Lesson.objects.filter(client__user=user).select_related(
|
||
'mentor', 'client', 'client__user', 'subject'
|
||
)
|
||
|
||
# Один запрос для всех подсчетов занятий
|
||
lessons_stats = lessons.aggregate(
|
||
total=Count('id'),
|
||
completed=Count('id', filter=Q(status='completed')),
|
||
this_week=Count('id', filter=Q(start_time__gte=week_ago))
|
||
)
|
||
|
||
total_lessons = lessons_stats['total']
|
||
completed_lessons = lessons_stats['completed']
|
||
lessons_this_week = lessons_stats['this_week']
|
||
|
||
# Ближайшие занятия с оптимизацией
|
||
upcoming_lessons = lessons.filter(
|
||
start_time__gte=now,
|
||
status__in=['scheduled', 'in_progress']
|
||
).select_related('mentor', 'client', 'client__user').order_by('start_time')[:5]
|
||
|
||
# Домашние задания - оптимизация
|
||
my_homeworks = Homework.objects.filter(assigned_to=user).select_related('mentor', 'lesson')
|
||
total_homeworks = my_homeworks.count()
|
||
|
||
my_submissions = HomeworkSubmission.objects.filter(student=user).select_related('homework', 'student')
|
||
|
||
# Один запрос для подсчетов отправок ДЗ
|
||
submissions_stats = my_submissions.aggregate(
|
||
completed=Count('id', filter=Q(passed=True)),
|
||
avg_score=Avg('score', filter=Q(status='graded'))
|
||
)
|
||
completed_homeworks = submissions_stats['completed']
|
||
average_score = submissions_stats['avg_score'] or 0
|
||
|
||
# Пending homeworks - оптимизация через exists
|
||
pending_homeworks = my_homeworks.filter(
|
||
status='published'
|
||
).exclude(
|
||
id__in=my_submissions.values('homework_id')
|
||
).count()
|
||
|
||
# Материалы (исправлено: lesson__client__user)
|
||
shared_materials = Material.objects.filter(
|
||
models.Q(shared_with=user) |
|
||
models.Q(access_type='public') |
|
||
models.Q(lesson__client__user=user)
|
||
).filter(is_deleted=False).distinct().count()
|
||
|
||
# Уведомления
|
||
unread_notifications = Notification.objects.filter(
|
||
recipient=user,
|
||
is_read=False
|
||
).count()
|
||
|
||
response_data = {
|
||
'summary': {
|
||
'total_lessons': total_lessons,
|
||
'completed_lessons': completed_lessons,
|
||
'lessons_this_week': lessons_this_week,
|
||
'total_homeworks': total_homeworks,
|
||
'completed_homeworks': completed_homeworks,
|
||
'pending_homeworks': pending_homeworks,
|
||
'average_score': round(average_score, 2),
|
||
'shared_materials': shared_materials,
|
||
'unread_notifications': unread_notifications
|
||
},
|
||
'upcoming_lessons': [
|
||
{
|
||
'id': lesson.id,
|
||
'title': lesson.title,
|
||
'mentor': {
|
||
'id': lesson.mentor.id,
|
||
'name': lesson.mentor.get_full_name()
|
||
},
|
||
'start_time': format_datetime_for_user(lesson.start_time, request.user.timezone) if lesson.start_time else None,
|
||
'end_time': format_datetime_for_user(lesson.end_time, request.user.timezone) if lesson.end_time else None
|
||
}
|
||
for lesson in upcoming_lessons
|
||
]
|
||
}
|
||
|
||
# Сохраняем в кеш на 2 минуты (120 секунд)
|
||
# Кеш на 30 секунд для актуальности уведомлений
|
||
cache.set(cache_key, response_data, 30)
|
||
|
||
return Response(response_data)
|
||
|
||
@action(detail=False, methods=['get'])
|
||
def progress(self, request):
|
||
"""
|
||
Прогресс обучения клиента.
|
||
|
||
GET /api/users/client/progress/
|
||
"""
|
||
user = request.user
|
||
|
||
# Период
|
||
period = request.query_params.get('period', '30')
|
||
days = int(period)
|
||
start_date = timezone.now() - timedelta(days=days)
|
||
|
||
# Занятия (исправлено: client__user)
|
||
lessons = Lesson.objects.filter(
|
||
client__user=user,
|
||
start_time__gte=start_date
|
||
).select_related('client', 'client__user', 'mentor', 'subject')
|
||
|
||
# Оптимизация: один запрос для статистики занятий
|
||
from django.db.models import Count, Q
|
||
lessons_stats = lessons.aggregate(
|
||
total=Count('id'),
|
||
completed=Count('id', filter=Q(status='completed'))
|
||
)
|
||
total_lessons = lessons_stats['total']
|
||
completed_lessons = lessons_stats['completed']
|
||
completion_rate = (completed_lessons / total_lessons * 100) if total_lessons > 0 else 0
|
||
|
||
# Домашние задания
|
||
submissions = HomeworkSubmission.objects.filter(
|
||
student=user,
|
||
submitted_at__gte=start_date
|
||
).select_related('homework', 'homework__lesson')
|
||
|
||
# Оптимизация: один запрос для статистики submissions
|
||
submissions_stats = submissions.filter(status='graded').aggregate(
|
||
total=models.Count('id'),
|
||
passed=models.Count('id', filter=models.Q(passed=True))
|
||
)
|
||
total_submissions = submissions_stats['total']
|
||
passed_submissions = submissions_stats['passed']
|
||
pass_rate = (passed_submissions / total_submissions * 100) if total_submissions > 0 else 0
|
||
|
||
# Прогресс по времени
|
||
progress_by_week = lessons.extra(
|
||
select={'week': 'EXTRACT(WEEK FROM start_time)'}
|
||
).values('week').annotate(
|
||
lessons_count=models.Count('id'),
|
||
completed_count=models.Count('id', filter=models.Q(status='completed'))
|
||
).order_by('week')
|
||
|
||
# Оценки по времени
|
||
scores_by_week = submissions.filter(
|
||
status='graded'
|
||
).extra(
|
||
select={'week': 'EXTRACT(WEEK FROM submitted_at)'}
|
||
).values('week').annotate(
|
||
average_score=models.Avg('score'),
|
||
count=models.Count('id')
|
||
).order_by('week')
|
||
|
||
# Получаем все занятия клиента (не только за период)
|
||
all_lessons = Lesson.objects.filter(client__user=user).select_related('client', 'client__user', 'mentor', 'subject')
|
||
|
||
# Оптимизация: один запрос для всех статистик
|
||
all_lessons_stats = all_lessons.aggregate(
|
||
total=models.Count('id'),
|
||
cancelled=models.Count('id', filter=models.Q(status='cancelled')),
|
||
avg_mentor_grade=models.Avg('mentor_grade', filter=models.Q(status='completed', mentor_grade__isnull=False)),
|
||
avg_school_grade=models.Avg('school_grade', filter=models.Q(status='completed', school_grade__isnull=False))
|
||
)
|
||
cancelled_lessons = all_lessons_stats['cancelled']
|
||
attendance_rate = ((all_lessons_stats['total'] - cancelled_lessons) / all_lessons_stats['total'] * 100) if all_lessons_stats['total'] > 0 else 0
|
||
average_mentor_grade = all_lessons_stats['avg_mentor_grade'] or 0
|
||
average_school_grade = all_lessons_stats['avg_school_grade'] or 0
|
||
|
||
# Домашние задания
|
||
all_homeworks = Homework.objects.filter(
|
||
lesson__client__user=user
|
||
).count()
|
||
|
||
completed_homeworks = HomeworkSubmission.objects.filter(
|
||
student=user,
|
||
status='graded'
|
||
).count()
|
||
|
||
homework_completion_rate = (completed_homeworks / all_homeworks * 100) if all_homeworks > 0 else 0
|
||
|
||
# Оценки по предметам
|
||
grades_by_subject = all_lessons.filter(
|
||
status='completed',
|
||
mentor_grade__isnull=False
|
||
).values('subject__name').annotate(
|
||
average_grade=models.Avg('mentor_grade'),
|
||
lessons_count=models.Count('id')
|
||
).order_by('-average_grade')[:5]
|
||
|
||
# Последние оценки
|
||
recent_grades = all_lessons.filter(
|
||
status='completed'
|
||
).exclude(
|
||
mentor_grade__isnull=True,
|
||
school_grade__isnull=True
|
||
).order_by('-start_time')[:5]
|
||
|
||
return Response({
|
||
'total_lessons': all_lessons.count(),
|
||
'completed_lessons': all_lessons.filter(status='completed').count(),
|
||
'cancelled_lessons': cancelled_lessons,
|
||
'attendance_rate': round(attendance_rate, 2),
|
||
'average_mentor_grade': round(average_mentor_grade, 2),
|
||
'average_school_grade': round(average_school_grade, 2),
|
||
'total_homework': all_homeworks,
|
||
'completed_homework': completed_homeworks,
|
||
'homework_completion_rate': round(homework_completion_rate, 2),
|
||
'grades_by_subject': [
|
||
{
|
||
'subject': item['subject__name'] or 'Без предмета',
|
||
'average_grade': round(item['average_grade'], 1),
|
||
'lessons_count': item['lessons_count']
|
||
}
|
||
for item in grades_by_subject
|
||
],
|
||
'recent_grades': [
|
||
{
|
||
'lesson_title': lesson.title,
|
||
'date': format_datetime_for_user(lesson.start_time, request.user.timezone) if lesson.start_time else None,
|
||
'mentor_grade': lesson.mentor_grade or 0,
|
||
'school_grade': lesson.school_grade or 0
|
||
}
|
||
for lesson in recent_grades
|
||
]
|
||
})
|
||
|
||
|
||
class ParentDashboardViewSet(viewsets.ViewSet):
|
||
"""
|
||
ViewSet для дашборда родителя.
|
||
|
||
dashboard: Основная статистика по детям
|
||
children: Список детей
|
||
child_report: Отчет по ребенку
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
@action(detail=False, methods=['get'])
|
||
def dashboard(self, request):
|
||
"""
|
||
Основной дашборд родителя.
|
||
|
||
GET /api/users/parent/dashboard/
|
||
"""
|
||
user = request.user
|
||
|
||
# Кеширование: кеш на 2 минуты для каждого пользователя
|
||
# ВАЖНО: Очищаем кеш при изменении структуры данных (добавлении mentors)
|
||
cache_key = f'parent_dashboard_v2_{user.id}' # Версия 2 с mentors
|
||
cached_data = cache.get(cache_key)
|
||
|
||
if cached_data is not None:
|
||
return Response(cached_data)
|
||
|
||
try:
|
||
parent = Parent.objects.get(user=user)
|
||
except Parent.DoesNotExist:
|
||
return Response(
|
||
{'error': 'Профиль родителя не найден'},
|
||
status=status.HTTP_404_NOT_FOUND
|
||
)
|
||
except Exception as e:
|
||
logger.error(f'Ошибка получения родителя для пользователя {user.id}: {e}', exc_info=True)
|
||
return Response(
|
||
{'error': 'Ошибка получения данных родителя'},
|
||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||
)
|
||
|
||
# Оптимизация: используем prefetch_related для избежания N+1 запросов
|
||
from django.db.models import Count, Avg, Q, Prefetch
|
||
|
||
# Получаем всех детей с предзагрузкой менторов одним запросом
|
||
children = list(parent.children.select_related('user').prefetch_related(
|
||
Prefetch('mentors', queryset=User.objects.only(
|
||
'id', 'email', 'first_name', 'last_name', 'avatar'
|
||
))
|
||
).all())
|
||
|
||
# Получаем всех детей одним запросом
|
||
children_users = [child.user for child in children]
|
||
children_user_ids = [user.id for user in children_users]
|
||
children_client_ids = [c.id for c in children]
|
||
|
||
# Получаем всех менторов всех детей одним запросом через промежуточную таблицу ManyToMany
|
||
mentors_by_child = defaultdict(list)
|
||
if children_client_ids:
|
||
# Получаем все связи менторов с детьми одним запросом
|
||
client_mentor_relations = list(Client.mentors.through.objects.filter(
|
||
client_id__in=children_client_ids
|
||
).values_list('client_id', 'user_id'))
|
||
|
||
# Группируем менторов по детям (используем defaultdict для эффективности)
|
||
# Это единственный необходимый цикл для группировки
|
||
for client_id, mentor_id in client_mentor_relations:
|
||
mentors_by_child[client_id].append(mentor_id)
|
||
|
||
# Получаем всех уникальных менторов одним запросом (set comprehension)
|
||
all_mentor_ids = {mentor_id for mentor_ids in mentors_by_child.values() for mentor_id in mentor_ids}
|
||
|
||
# Загружаем всех менторов одним запросом (dict comprehension)
|
||
mentors_dict = {
|
||
mentor.id: mentor for mentor in User.objects.filter(id__in=all_mentor_ids).only(
|
||
'id', 'email', 'first_name', 'last_name', 'avatar'
|
||
)
|
||
}
|
||
else:
|
||
mentors_dict = {}
|
||
|
||
# Оптимизация: один запрос для всех занятий всех детей
|
||
now = timezone.now()
|
||
all_upcoming_lessons = Lesson.objects.filter(
|
||
client__user_id__in=children_user_ids,
|
||
start_time__gte=now,
|
||
status__in=['scheduled', 'in_progress']
|
||
).select_related('client', 'client__user').values('client__user_id').annotate(
|
||
count=Count('id')
|
||
)
|
||
upcoming_lessons_by_child = {item['client__user_id']: item['count'] for item in all_upcoming_lessons}
|
||
|
||
# Оптимизация: один запрос для всех отправок ДЗ всех детей
|
||
all_submissions = HomeworkSubmission.objects.filter(
|
||
student_id__in=children_user_ids
|
||
).select_related('homework', 'student').values('student_id').annotate(
|
||
avg_score=Avg('score', filter=Q(status='graded'))
|
||
)
|
||
avg_scores_by_child = {item['student_id']: (item['avg_score'] or 0) for item in all_submissions}
|
||
|
||
# Оптимизация: один запрос для всех домашних заданий (assigned_to - ManyToManyField)
|
||
all_homeworks = list(Homework.objects.filter(
|
||
assigned_to__in=children_user_ids,
|
||
status='published'
|
||
).prefetch_related('assigned_to').distinct())
|
||
|
||
# Получаем отправленные ДЗ
|
||
submitted_homework_ids = set(
|
||
HomeworkSubmission.objects.filter(
|
||
student_id__in=children_user_ids
|
||
).values_list('homework_id', flat=True)
|
||
)
|
||
|
||
# Подсчитываем pending для каждого ребенка
|
||
pending_by_child = {user_id: 0 for user_id in children_user_ids}
|
||
for homework in all_homeworks:
|
||
# Оптимизация: используем предзагруженные данные через prefetch_related
|
||
# Преобразуем в список, чтобы гарантировать использование предзагруженных данных
|
||
assigned_user_ids = {user.id for user in list(homework.assigned_to.all())}
|
||
# Пересечение с детьми
|
||
children_assigned = assigned_user_ids & set(children_user_ids)
|
||
# Если ДЗ не отправлено, добавляем к счетчику для каждого назначенного ребенка
|
||
if homework.id not in submitted_homework_ids:
|
||
for user_id in children_assigned:
|
||
pending_by_child[user_id] = pending_by_child.get(user_id, 0) + 1
|
||
|
||
# Создаем данные детей с менторами (list comprehension с вложенным list comprehension)
|
||
children_data = [
|
||
{
|
||
'child': {
|
||
'id': child.user.id,
|
||
'name': child.user.get_full_name(),
|
||
'email': child.user.email,
|
||
'avatar': child.user.avatar.url if child.user.avatar else None,
|
||
'avatar_url': (
|
||
request.build_absolute_uri(child.user.avatar.url)
|
||
if child.user.avatar else None
|
||
),
|
||
},
|
||
'mentors': [
|
||
{
|
||
'id': mentor.id,
|
||
'email': mentor.email,
|
||
'first_name': mentor.first_name,
|
||
'last_name': mentor.last_name,
|
||
'avatar_url': (
|
||
request.build_absolute_uri(mentor.avatar.url)
|
||
if mentor.avatar else None
|
||
),
|
||
}
|
||
for mentor_id in mentors_by_child.get(child.id, [])
|
||
if (mentor := mentors_dict.get(mentor_id))
|
||
],
|
||
'summary': {
|
||
'upcoming_lessons': upcoming_lessons_by_child.get(child.user.id, 0),
|
||
'pending_homeworks': pending_by_child.get(child.user.id, 0),
|
||
'average_score': round(avg_scores_by_child.get(child.user.id, 0), 2)
|
||
}
|
||
}
|
||
for child in children
|
||
]
|
||
|
||
response_data = {
|
||
'children_count': len(children_data),
|
||
'children': children_data
|
||
}
|
||
|
||
# Сохраняем в кеш на 2 минуты (120 секунд)
|
||
# Кеш на 30 секунд для актуальности уведомлений
|
||
try:
|
||
cache.set(cache_key, response_data, 30)
|
||
except Exception as e:
|
||
logger.warning(f'Не удалось сохранить в кеш: {e}')
|
||
|
||
return Response(response_data)
|
||
|
||
@action(detail=True, methods=['get'])
|
||
def child_report(self, request, pk=None):
|
||
"""
|
||
Детальный отчет по ребенку.
|
||
|
||
GET /api/users/parent/{child_id}/child_report/
|
||
"""
|
||
user = request.user
|
||
|
||
try:
|
||
parent = Parent.objects.get(user=user)
|
||
except Parent.DoesNotExist:
|
||
return Response(
|
||
{'error': 'Профиль родителя не найден'},
|
||
status=status.HTTP_404_NOT_FOUND
|
||
)
|
||
|
||
# Проверяем что это ребенок родителя
|
||
try:
|
||
child = parent.children.get(user_id=pk)
|
||
except Client.DoesNotExist:
|
||
return Response(
|
||
{'error': 'Ребенок не найден'},
|
||
status=status.HTTP_404_NOT_FOUND
|
||
)
|
||
|
||
# Период
|
||
period = request.query_params.get('period', '30')
|
||
days = int(period)
|
||
start_date = timezone.now() - timedelta(days=days)
|
||
|
||
# Получаем User объект ребенка
|
||
child_user = child.user
|
||
|
||
# Занятия
|
||
lessons = Lesson.objects.filter(
|
||
client=child,
|
||
start_time__gte=start_date
|
||
)
|
||
|
||
# Домашние задания
|
||
submissions = HomeworkSubmission.objects.filter(
|
||
student=child_user,
|
||
submitted_at__gte=start_date
|
||
)
|
||
|
||
# Материалы
|
||
materials = Material.objects.filter(
|
||
models.Q(shared_with=child_user) |
|
||
models.Q(lesson__client=child)
|
||
).filter(
|
||
is_deleted=False,
|
||
created_at__gte=start_date
|
||
).distinct()
|
||
|
||
return Response({
|
||
'child': {
|
||
'id': child.id,
|
||
'name': child_user.get_full_name(),
|
||
'email': child_user.email
|
||
},
|
||
'period_days': days,
|
||
'lessons': {
|
||
'total': lessons.count(),
|
||
'completed': lessons.filter(status='completed').count(),
|
||
'upcoming': lessons.filter(
|
||
start_time__gte=timezone.now(),
|
||
status='confirmed'
|
||
).count()
|
||
},
|
||
'homeworks': {
|
||
'total_submissions': submissions.count(),
|
||
'graded': submissions.filter(status='graded').count(),
|
||
'passed': submissions.filter(passed=True).count(),
|
||
'average_score': submissions.filter(
|
||
status='graded'
|
||
).aggregate(avg=models.Avg('score'))['avg'] or 0
|
||
},
|
||
'materials': {
|
||
'total': materials.count(),
|
||
'by_type': list(materials.values('material_type').annotate(
|
||
count=models.Count('id')
|
||
))
|
||
}
|
||
})
|
||
|
||
@action(detail=True, methods=['get'], url_path='child_dashboard')
|
||
def child_dashboard(self, request, pk=None):
|
||
"""
|
||
Дашборд выбранного ребенка для родителя.
|
||
Возвращает данные в формате, аналогичном /client/dashboard/
|
||
|
||
GET /api/users/parent/{child_id}/child_dashboard/
|
||
"""
|
||
user = request.user
|
||
|
||
try:
|
||
parent = Parent.objects.get(user=user)
|
||
except Parent.DoesNotExist:
|
||
return Response(
|
||
{'error': 'Профиль родителя не найден'},
|
||
status=status.HTTP_404_NOT_FOUND
|
||
)
|
||
|
||
# Проверяем что это ребенок родителя
|
||
try:
|
||
child = parent.children.get(user_id=pk)
|
||
except Client.DoesNotExist:
|
||
return Response(
|
||
{'error': 'Ребенок не найден'},
|
||
status=status.HTTP_404_NOT_FOUND
|
||
)
|
||
|
||
# Получаем User объект ребенка
|
||
child_user = child.user
|
||
|
||
# Кеширование: кеш на 2 минуты для каждого ребенка
|
||
cache_key = f'parent_child_dashboard_{user.id}_{child_user.id}'
|
||
cached_data = cache.get(cache_key)
|
||
|
||
if cached_data is not None:
|
||
return Response(cached_data)
|
||
|
||
# Временные рамки
|
||
now = timezone.now()
|
||
week_ago = now - timedelta(days=7)
|
||
month_ago = now - timedelta(days=30)
|
||
|
||
# Занятия - используем ту же логику, что и для клиента
|
||
from django.db.models import Count, Avg, Q
|
||
lessons = Lesson.objects.filter(client__user=child_user).select_related(
|
||
'mentor', 'client', 'client__user', 'subject'
|
||
)
|
||
|
||
# Один запрос для всех подсчетов занятий
|
||
lessons_stats = lessons.aggregate(
|
||
total=Count('id'),
|
||
completed=Count('id', filter=Q(status='completed')),
|
||
this_week=Count('id', filter=Q(start_time__gte=week_ago))
|
||
)
|
||
|
||
total_lessons = lessons_stats['total']
|
||
completed_lessons = lessons_stats['completed']
|
||
lessons_this_week = lessons_stats['this_week']
|
||
|
||
# Ближайшие занятия
|
||
upcoming_lessons = lessons.filter(
|
||
start_time__gte=now,
|
||
status__in=['scheduled', 'in_progress']
|
||
).select_related('mentor', 'client', 'client__user').order_by('start_time')[:5]
|
||
|
||
# Домашние задания
|
||
my_homeworks = Homework.objects.filter(assigned_to=child_user).select_related('mentor', 'lesson')
|
||
total_homeworks = my_homeworks.count()
|
||
|
||
my_submissions = HomeworkSubmission.objects.filter(student=child_user).select_related('homework', 'student')
|
||
|
||
# Один запрос для подсчетов отправок ДЗ
|
||
submissions_stats = my_submissions.aggregate(
|
||
completed=Count('id', filter=Q(passed=True)),
|
||
avg_score=Avg('score', filter=Q(status='graded'))
|
||
)
|
||
completed_homeworks = submissions_stats['completed']
|
||
average_score = submissions_stats['avg_score'] or 0
|
||
|
||
# Pending homeworks
|
||
pending_homeworks = my_homeworks.filter(
|
||
status='published'
|
||
).exclude(
|
||
id__in=my_submissions.values('homework_id')
|
||
).count()
|
||
|
||
# Материалы
|
||
shared_materials = Material.objects.filter(
|
||
models.Q(shared_with=child_user) |
|
||
models.Q(access_type='public') |
|
||
models.Q(lesson__client__user=child_user)
|
||
).filter(is_deleted=False).distinct().count()
|
||
|
||
# Уведомления (для ребенка)
|
||
unread_notifications = Notification.objects.filter(
|
||
recipient=child_user,
|
||
is_read=False
|
||
).count()
|
||
|
||
response_data = {
|
||
'summary': {
|
||
'total_lessons': total_lessons,
|
||
'completed_lessons': completed_lessons,
|
||
'lessons_this_week': lessons_this_week,
|
||
'total_homeworks': total_homeworks,
|
||
'completed_homeworks': completed_homeworks,
|
||
'pending_homeworks': pending_homeworks,
|
||
'average_score': round(average_score, 2),
|
||
'shared_materials': shared_materials,
|
||
'unread_notifications': unread_notifications
|
||
},
|
||
'upcoming_lessons': [
|
||
{
|
||
'id': lesson.id,
|
||
'title': lesson.title,
|
||
'mentor': {
|
||
'id': lesson.mentor.id,
|
||
'name': lesson.mentor.get_full_name()
|
||
},
|
||
'start_time': format_datetime_for_user(lesson.start_time, request.user.timezone) if lesson.start_time else None,
|
||
'end_time': format_datetime_for_user(lesson.end_time, request.user.timezone) if lesson.end_time else None
|
||
}
|
||
for lesson in upcoming_lessons
|
||
]
|
||
}
|
||
|
||
# Сохраняем в кеш на 2 минуты (120 секунд)
|
||
cache.set(cache_key, response_data, 30)
|
||
|
||
return Response(response_data)
|
||
|