uchill/backend/apps/schedule/serializers.py

789 lines
36 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Сериализаторы для расписания.
"""
from rest_framework import serializers
from django.utils import timezone
from datetime import datetime, timedelta
from .models import Lesson, LessonTemplate, TimeSlot, Availability, LessonFile, LessonHomeworkSubmission, Subject, MentorSubject
from apps.users.serializers import UserSerializer, ClientSerializer
from apps.users.utils import format_datetime_for_user, get_user_timezone
from django.utils import timezone as django_timezone
import pytz
class LessonTemplateSerializer(serializers.ModelSerializer):
"""Сериализатор для шаблона занятия."""
mentor_name = serializers.CharField(source='mentor.get_full_name', read_only=True)
lessons_count = serializers.SerializerMethodField()
class Meta:
model = LessonTemplate
fields = [
'id', 'mentor', 'mentor_name', 'title', 'description',
'subject', 'duration', 'is_active', 'meeting_url',
'color', 'lessons_count', 'created_at', 'updated_at'
]
read_only_fields = ['id', 'created_at', 'updated_at']
def get_lessons_count(self, obj):
"""Количество занятий созданных из шаблона."""
return obj.lessons.count()
class LessonSerializer(serializers.ModelSerializer):
"""Базовый сериализатор для занятия."""
mentor_name = serializers.CharField(source='mentor.get_full_name', read_only=True)
client_name = serializers.CharField(source='client.user.get_full_name', read_only=True)
group_name = serializers.CharField(source='group.name', read_only=True, allow_null=True)
template_title = serializers.CharField(source='template.title', read_only=True, allow_null=True)
# Предмет как строка (название) для удобства на фронтенде
subject = serializers.SerializerMethodField()
# Цена как число (DecimalField возвращает строку)
price = serializers.DecimalField(max_digits=10, decimal_places=2, required=False, allow_null=True, coerce_to_string=False)
# Ссылка на встречу - разрешаем пустую строку
meeting_url = serializers.URLField(required=False, allow_blank=True, allow_null=True)
# Файлы урока
files = serializers.SerializerMethodField()
# Вычисляемые поля
is_upcoming = serializers.BooleanField(read_only=True)
is_past = serializers.BooleanField(read_only=True)
is_in_progress = serializers.BooleanField(read_only=True)
can_be_cancelled = serializers.BooleanField(read_only=True)
can_be_rescheduled = serializers.BooleanField(read_only=True)
def get_subject(self, obj):
"""Возвращаем название предмета вместо ID."""
if obj.subject:
return obj.subject.name
if obj.mentor_subject:
return obj.mentor_subject.name
if obj.subject_name:
return obj.subject_name
return None
def get_files(self, obj):
"""Получить файлы урока."""
files = obj.files.all()
return LessonFileSerializer(files, many=True).data
def to_representation(self, instance):
"""Переопределяем для конвертации времени в часовой пояс пользователя."""
data = super().to_representation(instance)
request = self.context.get('request')
user_timezone = 'UTC'
if request and hasattr(request, 'user') and request.user.is_authenticated:
user_timezone = getattr(request.user, 'timezone', None) or 'UTC'
datetime_fields = ['start_time', 'end_time', 'completed_at', 'created_at', 'updated_at']
for field in datetime_fields:
if field in data:
field_value = getattr(instance, field, None)
if field_value:
data[field] = format_datetime_for_user(field_value, user_timezone)
return data
class Meta:
model = Lesson
fields = [
'id', 'mentor', 'mentor_name', 'client', 'client_name', 'group', 'group_name',
'start_time', 'end_time', 'duration', 'title', 'description',
'subject', 'subject_name', 'mentor_subject', 'status', 'template', 'template_title',
'meeting_url', 'mentor_notes', 'homework_text', 'mentor_grade', 'school_grade',
'price', 'reminder_sent', 'files', 'is_upcoming', 'is_past', 'is_in_progress',
'can_be_cancelled', 'can_be_rescheduled',
'is_recurring', 'recurring_series_id', 'parent_lesson',
'completed_at', 'created_at', 'updated_at',
'livekit_room_name'
]
read_only_fields = [
'id', 'end_time', 'reminder_sent',
'created_at', 'updated_at', 'livekit_room_name'
]
def validate_meeting_url(self, value):
"""Нормализация meeting_url - пустая строка становится None."""
if value == '':
return None
return value
def validate(self, attrs):
"""Валидация данных занятия."""
# Для завершённых занятий разрешаем менять только price и status
if self.instance and self.instance.status == 'completed':
allowed = {'price', 'status'}
attrs = {k: v for k, v in attrs.items() if k in allowed}
if 'status' in attrs and attrs['status'] not in ('completed', 'cancelled'):
raise serializers.ValidationError({
'status': 'Для завершённого занятия можно только оставить "Завершено" или пометить как "Отменено"'
})
return attrs
# Нормализуем meeting_url - пустая строка становится None
if 'meeting_url' in attrs and attrs['meeting_url'] == '':
attrs['meeting_url'] = None
start_time = attrs.get('start_time')
duration = attrs.get('duration', 60)
# Проверка конфликтов (только при создании или изменении времени)
if self.instance is None or 'start_time' in attrs:
mentor = attrs.get('mentor') or self.instance.mentor if self.instance else None
client = attrs.get('client') or self.instance.client if self.instance else None
if mentor and start_time:
end_time = start_time + timedelta(minutes=duration)
# Проверка конфликтов для ментора
mentor_conflicts = Lesson.objects.filter(
mentor=mentor,
status='scheduled',
start_time__lt=end_time,
end_time__gt=start_time
).exclude(pk=self.instance.pk if self.instance else None)
if mentor_conflicts.exists():
raise serializers.ValidationError({
'start_time': 'У ментора уже есть занятие в это время'
})
# Проверка конфликтов для клиента
if client:
client_conflicts = Lesson.objects.filter(
client=client,
status='scheduled',
start_time__lt=end_time,
end_time__gt=start_time
).exclude(pk=self.instance.pk if self.instance else None)
if client_conflicts.exists():
raise serializers.ValidationError({
'start_time': 'У клиента уже есть занятие в это время'
})
return attrs
class LessonDetailSerializer(LessonSerializer):
"""Детальный сериализатор для занятия с полной информацией."""
mentor = UserSerializer(read_only=True)
client = ClientSerializer(read_only=True)
template = LessonTemplateSerializer(read_only=True)
cancelled_by_name = serializers.CharField(
source='cancelled_by.get_full_name',
read_only=True,
allow_null=True
)
class Meta(LessonSerializer.Meta):
fields = LessonSerializer.Meta.fields + [
'cancelled_by', 'cancelled_by_name', 'cancellation_reason',
'cancelled_at', 'rescheduled_from'
]
class LessonCreateSerializer(serializers.ModelSerializer):
"""Сериализатор для создания занятия."""
mentor = serializers.HiddenField(default=serializers.CurrentUserDefault())
subject_id = serializers.IntegerField(required=False, allow_null=True, source='subject')
mentor_subject_id = serializers.IntegerField(required=False, allow_null=True, source='mentor_subject')
subject_name = serializers.CharField(required=False, allow_blank=True, max_length=100)
price = serializers.DecimalField(max_digits=10, decimal_places=2, required=True, coerce_to_string=False)
class Meta:
model = Lesson
fields = [
'mentor', 'client', 'group', 'start_time', 'duration',
'title', 'description', 'subject_id', 'mentor_subject_id', 'subject_name', 'template', 'price',
'is_recurring'
]
extra_kwargs = {
'client': {'required': False, 'allow_null': True},
'group': {'required': False, 'allow_null': True},
}
def to_internal_value(self, data):
"""
Переопределяем для обработки start_time перед валидацией.
Фронтенд отправляет время в UTC (через .toISOString()), но это время уже
было конвертировано из локального времени браузера в UTC. Нам нужно
интерпретировать это время как локальное время пользователя (из его профиля)
и конвертировать в UTC для сохранения.
Пример:
- Пользователь с UTC+4 вводит "11.01.2025 22:15" в input
- Фронтенд конвертирует в UTC: "2025-01-11T18:15:00Z" (22:15 - 4 = 18:15)
- Бэкенд должен интерпретировать "18:15 UTC" как "22:15 UTC+4" и сохранить как "18:15 UTC"
- Но это неправильно! Нужно интерпретировать "18:15 UTC" как "18:15 UTC+4" = "14:15 UTC"
Правильный подход:
- Фронтенд отправляет время в UTC, но мы должны знать, что это время было
введено пользователем в его локальном часовом поясе
- Поэтому мы берем UTC время, интерпретируем его как локальное время пользователя,
и конвертируем обратно в UTC
"""
# Получаем часовой пояс пользователя из request
request = self.context.get('request')
user_timezone = 'UTC'
if request and hasattr(request, 'user') and request.user.is_authenticated:
user_timezone = request.user.timezone or 'UTC'
# Если start_time приходит как строка ISO в UTC (с 'Z' в конце)
# Фронтенд отправляет время в UTC, но это время было конвертировано из локального времени браузера.
# Нам нужно интерпретировать это время как локальное время пользователя (из профиля) и конвертировать в UTC.
if 'start_time' in data and isinstance(data['start_time'], str):
try:
# Парсим ISO строку
dt_str = data['start_time'].replace('Z', '+00:00')
dt_parsed = datetime.fromisoformat(dt_str)
# Конвертируем в pytz.UTC для единообразия
if dt_parsed.tzinfo is None:
# Если naive, интерпретируем как UTC
dt_utc = pytz.UTC.localize(dt_parsed)
elif dt_parsed.tzinfo == pytz.UTC:
dt_utc = dt_parsed
else:
# Если другой timezone (например, timezone.utc из стандартной библиотеки), конвертируем в pytz.UTC
dt_utc = dt_parsed.astimezone(pytz.UTC)
# Фронтенд уже правильно конвертировал время из локального времени браузера в UTC
# Просто сохраняем как есть, не делаем двойную конвертацию
data['start_time'] = dt_utc.isoformat()
except (ValueError, AttributeError, TypeError) as e:
# Если не удалось распарсить, оставляем как есть
import logging
logger = logging.getLogger(__name__)
logger.warning(f"Error parsing start_time: {e}, original value: {data.get('start_time')}")
return super().to_internal_value(data)
def validate_price(self, value):
"""Валидация стоимости - должно быть больше 0."""
if value is None:
raise serializers.ValidationError('Стоимость обязательна для указания')
if value <= 0:
raise serializers.ValidationError('Стоимость должна быть больше 0')
return value
def validate_start_time(self, value):
"""
Дополнительная валидация start_time.
Убеждаемся, что время в UTC и aware.
"""
if value is None:
return value
# Если время уже aware (с timezone), проверяем, нужно ли конвертировать
if django_timezone.is_aware(value):
# Если timezone не UTC, конвертируем в UTC
if value.tzinfo != pytz.UTC:
return value.astimezone(pytz.UTC)
return value
# Если время naive (без timezone), интерпретируем его как UTC
try:
return pytz.UTC.localize(value)
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.warning(f"Error converting start_time to UTC: {e}")
return value
def validate(self, attrs):
"""Валидация при создании."""
start_time = attrs.get('start_time')
duration = attrs.get('duration', 60)
mentor = attrs.get('mentor')
client = attrs.get('client')
group = attrs.get('group')
if not client and not group:
raise serializers.ValidationError({
'client': 'Необходимо указать ученика или группу.'
})
# Проверка что указан либо subject_id, либо mentor_subject_id, либо subject_name
# subject_id и mentor_subject_id приходят через source='subject' и source='mentor_subject'
# поэтому они попадают в attrs как числа (ID), а не как экземпляры
subject_id = attrs.get('subject') # Это будет ID из-за source='subject'
mentor_subject_id = attrs.get('mentor_subject') # Это будет ID из-за source='mentor_subject'
subject_name = attrs.get('subject_name', '')
# Очищаем attrs от ID, чтобы установить правильные экземпляры
if 'subject' in attrs and isinstance(attrs['subject'], (int, str)):
# Сохраняем ID для дальнейшей обработки
subject_id = int(attrs['subject']) if isinstance(attrs['subject'], str) else attrs['subject']
attrs.pop('subject') # Удаляем ID из attrs
if 'mentor_subject' in attrs and isinstance(attrs['mentor_subject'], (int, str)):
# Сохраняем ID для дальнейшей обработки
mentor_subject_id = int(attrs['mentor_subject']) if isinstance(attrs['mentor_subject'], str) else attrs['mentor_subject']
attrs.pop('mentor_subject') # Удаляем ID из attrs
if not subject_id and not mentor_subject_id and not subject_name:
raise serializers.ValidationError({
'subject': 'Необходимо указать предмет'
})
# Если указан subject_id, проверяем что предмет существует и активен
if subject_id:
try:
from .models import Subject
subject = Subject.objects.get(id=subject_id, is_active=True)
attrs['subject'] = subject # Устанавливаем экземпляр модели
attrs['subject_name'] = subject.name
except Subject.DoesNotExist:
raise serializers.ValidationError({
'subject_id': 'Предмет не найден или неактивен'
})
# Если указан mentor_subject_id, проверяем что он принадлежит ментору
if mentor_subject_id:
try:
from .models import MentorSubject
mentor_subject = MentorSubject.objects.get(id=mentor_subject_id, mentor=mentor)
attrs['mentor_subject'] = mentor_subject # Устанавливаем экземпляр модели
attrs['subject_name'] = mentor_subject.name
# Увеличиваем счетчик использования
mentor_subject.increment_usage()
except MentorSubject.DoesNotExist:
raise serializers.ValidationError({
'mentor_subject_id': 'Кастомный предмет не найден или не принадлежит вам'
})
# Если указан только subject_name (кастомный предмет), создаем MentorSubject
if subject_name and not subject_id and not mentor_subject_id:
from .models import MentorSubject
# Проверяем, нет ли уже такого предмета у ментора
existing = MentorSubject.objects.filter(
mentor=mentor,
name__iexact=subject_name.strip()
).first()
if existing:
# Используем существующий
attrs['mentor_subject'] = existing
attrs['subject_name'] = existing.name
existing.increment_usage()
else:
# Создаем новый
mentor_subject = MentorSubject.objects.create(
mentor=mentor,
name=subject_name.strip()
)
attrs['mentor_subject'] = mentor_subject
attrs['subject_name'] = mentor_subject.name
# Нормализуем start_time к UTC
if start_time:
if not django_timezone.is_aware(start_time):
start_time = pytz.UTC.localize(start_time)
elif start_time.tzinfo != pytz.UTC:
start_time = start_time.astimezone(pytz.UTC)
# Проверяем что занятие не начинается более 30 минут назад
if start_time < django_timezone.now() - timedelta(minutes=30):
raise serializers.ValidationError({
'start_time': 'Нельзя создать занятие, время начала которого было более 30 минут назад'
})
# Рассчитываем время окончания
end_time = start_time + timedelta(minutes=duration)
# Проверка конфликтов для ментора
if mentor:
mentor_conflicts = Lesson.objects.filter(
mentor=mentor,
status__in=['scheduled', 'in_progress']
).filter(
start_time__lt=end_time,
end_time__gt=start_time
)
# Исключаем текущее занятие при редактировании
if self.instance:
mentor_conflicts = mentor_conflicts.exclude(id=self.instance.id)
if mentor_conflicts.exists():
conflict = mentor_conflicts.first()
raise serializers.ValidationError({
'start_time': f'У вас уже есть занятие в это время: "{conflict.title}" ({conflict.start_time.strftime("%H:%M")} - {conflict.end_time.strftime("%H:%M")})'
})
# Проверка конфликтов для студента
if client:
client_conflicts = Lesson.objects.filter(
client=client,
status__in=['scheduled', 'in_progress']
).filter(
start_time__lt=end_time,
end_time__gt=start_time
)
# Исключаем текущее занятие при редактировании
if self.instance:
client_conflicts = client_conflicts.exclude(id=self.instance.id)
if client_conflicts.exists():
conflict = client_conflicts.first()
raise serializers.ValidationError({
'start_time': f'У студента уже есть занятие в это время: "{conflict.title}" ({conflict.start_time.strftime("%H:%M")} - {conflict.end_time.strftime("%H:%M")})'
})
return attrs
class LessonCancelSerializer(serializers.Serializer):
"""Сериализатор для отмены занятия."""
cancellation_reason = serializers.CharField(
required=False,
allow_blank=True,
max_length=500
)
class LessonRescheduleSerializer(serializers.Serializer):
"""Сериализатор для переноса занятия."""
new_start_time = serializers.DateTimeField(required=True)
def validate_new_start_time(self, value):
"""Проверка нового времени."""
if value <= timezone.now():
raise serializers.ValidationError(
'Новое время должно быть в будущем'
)
return value
class TimeSlotSerializer(serializers.ModelSerializer):
"""Сериализатор для временного слота."""
mentor_name = serializers.CharField(source='mentor.get_full_name', read_only=True)
lesson_title = serializers.CharField(source='lesson.title', read_only=True, allow_null=True)
class Meta:
model = TimeSlot
fields = [
'id', 'mentor', 'mentor_name', 'start_time', 'end_time',
'is_available', 'is_booked', 'lesson', 'lesson_title',
'is_recurring', 'recurring_day', 'created_at', 'updated_at'
]
read_only_fields = ['id', 'is_booked', 'lesson', 'created_at', 'updated_at']
class AvailabilitySerializer(serializers.ModelSerializer):
"""Сериализатор для доступности."""
mentor_name = serializers.CharField(source='mentor.get_full_name', read_only=True)
day_name = serializers.CharField(source='get_day_of_week_display', read_only=True, allow_null=True)
class Meta:
model = Availability
fields = [
'id', 'mentor', 'mentor_name', 'day_of_week', 'day_name',
'specific_date', 'start_time', 'end_time', 'is_recurring',
'is_active', 'exception_dates', 'notes',
'created_at', 'updated_at'
]
read_only_fields = ['id', 'created_at', 'updated_at']
def validate(self, attrs):
"""Валидация доступности."""
is_recurring = attrs.get('is_recurring', True)
day_of_week = attrs.get('day_of_week')
specific_date = attrs.get('specific_date')
start_time = attrs.get('start_time')
end_time = attrs.get('end_time')
# Проверка что указан либо день недели, либо конкретная дата
if is_recurring:
if day_of_week is None:
raise serializers.ValidationError({
'day_of_week': 'Укажите день недели для повторяющейся доступности'
})
else:
if specific_date is None:
raise serializers.ValidationError({
'specific_date': 'Укажите конкретную дату для разовой доступности'
})
# Проверка времени
if start_time and end_time and start_time >= end_time:
raise serializers.ValidationError({
'end_time': 'Время окончания должно быть позже времени начала'
})
return attrs
class LessonFileSerializer(serializers.ModelSerializer):
"""Сериализатор для файлов уроков."""
file_url = serializers.SerializerMethodField()
file_size_display = serializers.SerializerMethodField()
uploaded_by_name = serializers.CharField(source='uploaded_by.get_full_name', read_only=True)
class Meta:
model = LessonFile
fields = [
'id', 'lesson', 'file', 'material', 'source', 'filename',
'file_size', 'file_size_display', 'file_url', 'description',
'uploaded_by', 'uploaded_by_name', 'created_at'
]
read_only_fields = ['id', 'uploaded_by', 'created_at']
def get_file_url(self, obj):
"""Получить URL файла."""
return obj.get_file_url()
def get_file_size_display(self, obj):
"""Отформатированный размер файла."""
if obj.file_size:
size = obj.file_size
for unit in ['B', 'KB', 'MB', 'GB']:
if size < 1024.0:
return f"{size:.1f} {unit}"
size /= 1024.0
return f"{size:.1f} TB"
return '0 B'
class LessonFileCreateSerializer(serializers.ModelSerializer):
"""Сериализатор для создания файла урока."""
# Разрешаем не передавать filename/file_size в запросе - они будут заполнены автоматически
filename = serializers.CharField(required=False, allow_blank=True)
file_size = serializers.IntegerField(required=False, allow_null=True)
class Meta:
model = LessonFile
fields = [
'id', 'lesson', 'file', 'material', 'source', 'filename',
'file_size', 'description'
]
read_only_fields = ['id']
def validate(self, attrs):
"""Валидация: должен быть либо file, либо material."""
file = attrs.get('file')
material = attrs.get('material')
if not file and not material:
raise serializers.ValidationError(
'Необходимо указать либо файл для загрузки, либо материал из библиотеки'
)
if file and material:
raise serializers.ValidationError(
'Нельзя указать одновременно файл и материал'
)
# Если файл загружается, проверяем размер
if file:
max_size = 10 * 1024 * 1024 # 10 MB
if file.size > max_size:
raise serializers.ValidationError(
f'Размер файла не должен превышать 10 МБ. Текущий размер: {file.size / (1024*1024):.2f} МБ'
)
# Сохраняем размер и имя файла
attrs['file_size'] = file.size
if not attrs.get('filename'):
attrs['filename'] = file.name
attrs['source'] = 'uploaded'
else:
# Если выбран материал, берем данные из него
if not attrs.get('filename'):
attrs['filename'] = material.title if material else 'Материал'
if material and material.file:
attrs['file_size'] = material.file.size
attrs['source'] = 'material'
return attrs
class LessonCalendarSerializer(serializers.Serializer):
"""Сериализатор для календаря занятий."""
start_date = serializers.DateField(required=True)
end_date = serializers.DateField(required=True)
mentor_id = serializers.IntegerField(required=False)
client_id = serializers.IntegerField(required=False)
status = serializers.ChoiceField(
choices=['scheduled', 'in_progress', 'completed', 'cancelled', 'rescheduled'],
required=False
)
def validate(self, attrs):
"""Валидация диапазона дат."""
start_date = attrs.get('start_date')
end_date = attrs.get('end_date')
if start_date and end_date and start_date > end_date:
raise serializers.ValidationError({
'end_date': 'Дата окончания должна быть позже даты начала'
})
# Ограничение диапазона (не более 6 месяцев — для календаря, смена месяцев)
if start_date and end_date:
delta = end_date - start_date
if delta.days > 180:
raise serializers.ValidationError(
'Диапазон не может превышать 180 дней'
)
return attrs
class LessonCalendarItemSerializer(serializers.ModelSerializer):
"""Лёгкий сериализатор для календаря: только поля, нужные для списка и календаря."""
client_name = serializers.CharField(source='client.user.get_full_name', read_only=True)
mentor_name = serializers.CharField(source='mentor.get_full_name', read_only=True)
group_name = serializers.CharField(source='group.name', read_only=True, default=None)
subject = serializers.SerializerMethodField()
def get_subject(self, obj):
if obj.subject:
return obj.subject.name
if obj.mentor_subject:
return obj.mentor_subject.name
return obj.subject_name
def to_representation(self, instance):
data = super().to_representation(instance)
request = self.context.get('request')
user_timezone = 'UTC'
if request and hasattr(request, 'user') and request.user.is_authenticated:
user_timezone = getattr(request.user, 'timezone', None) or 'UTC'
for field in ('start_time', 'end_time'):
val = getattr(instance, field, None)
if val:
data[field] = format_datetime_for_user(val, user_timezone)
return data
class Meta:
model = Lesson
fields = ['id', 'title', 'start_time', 'end_time', 'status', 'client', 'client_name', 'mentor', 'mentor_name', 'group', 'group_name', 'subject', 'subject_name']
class LessonHomeworkSubmissionSerializer(serializers.ModelSerializer):
"""Сериализатор для ответа на ДЗ по уроку."""
student_name = serializers.CharField(source='student.get_full_name', read_only=True)
student_email = serializers.CharField(source='student.email', read_only=True)
checked_by_name = serializers.CharField(source='checked_by.get_full_name', read_only=True)
attachment_url = serializers.SerializerMethodField()
class Meta:
model = LessonHomeworkSubmission
fields = [
'id', 'lesson', 'student', 'student_name', 'student_email',
'content', 'attachment', 'attachment_url',
'status', 'score', 'feedback',
'checked_by', 'checked_by_name', 'checked_at',
'submitted_at', 'updated_at'
]
read_only_fields = ['id', 'submitted_at', 'updated_at', 'checked_at']
def get_attachment_url(self, obj):
"""Получить URL файла."""
if obj.attachment:
request = self.context.get('request')
if request:
return request.build_absolute_uri(obj.attachment.url)
return obj.attachment.url
return None
class LessonHomeworkSubmissionCreateSerializer(serializers.ModelSerializer):
"""Сериализатор для создания ответа на ДЗ."""
class Meta:
model = LessonHomeworkSubmission
fields = ['lesson', 'content', 'attachment']
def create(self, validated_data):
"""Создать ответ на ДЗ."""
validated_data['student'] = self.context['request'].user
return super().create(validated_data)
class LessonHomeworkSubmissionGradeSerializer(serializers.Serializer):
"""Сериализатор для оценки ответа на ДЗ."""
score = serializers.IntegerField(
required=True,
min_value=0,
max_value=100,
help_text='Оценка от 0 до 100'
)
feedback = serializers.CharField(
required=False,
allow_blank=True,
help_text='Отзыв ментора'
)
class SubjectSerializer(serializers.ModelSerializer):
"""Сериализатор для предмета."""
class Meta:
model = Subject
fields = ['id', 'name', 'is_active', 'created_at', 'updated_at']
read_only_fields = ['id', 'created_at', 'updated_at']
class MentorSubjectSerializer(serializers.ModelSerializer):
"""Сериализатор для кастомного предмета ментора."""
mentor_name = serializers.CharField(source='mentor.get_full_name', read_only=True)
class Meta:
model = MentorSubject
fields = ['id', 'mentor', 'mentor_name', 'name', 'usage_count', 'created_at', 'updated_at']
read_only_fields = ['id', 'usage_count', 'created_at', 'updated_at']
class MentorSubjectCreateSerializer(serializers.ModelSerializer):
"""Сериализатор для создания кастомного предмета ментора."""
mentor = serializers.HiddenField(default=serializers.CurrentUserDefault())
class Meta:
model = MentorSubject
fields = ['mentor', 'name']
def validate_name(self, value):
"""Валидация названия предмета."""
if not value or not value.strip():
raise serializers.ValidationError('Название предмета не может быть пустым')
return value.strip()
def validate(self, attrs):
"""Проверка, что у ментора еще нет такого предмета."""
mentor = attrs.get('mentor')
name = attrs.get('name')
if mentor and name:
existing = MentorSubject.objects.filter(
mentor=mentor,
name__iexact=name
).first()
if existing:
raise serializers.ValidationError({
'name': 'У вас уже есть предмет с таким названием'
})
return attrs