""" Сериализаторы для расписания. """ 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) # Проверка: допускаем создание занятий до 30 минут в прошлом now = timezone.now() tolerance = timedelta(minutes=30) if start_time and start_time < now - tolerance: raise serializers.ValidationError({ 'start_time': 'Нельзя создать занятие, время начала которого было более 30 минут назад' }) # Проверка конфликтов (только при создании или изменении времени) 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' ] 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') # Проверка что указан либо 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 # Проверка: допускаем создание занятий до 30 минут в прошлом 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) now = django_timezone.now() tolerance = timedelta(minutes=30) if start_time < now - tolerance: 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) 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', '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