from rest_framework import serializers from utils import FileFieldSerializer, get_thumbs from apps.course.models import ( Course, CourseCategory, CourseChapter, CourseLesson, Lesson, CourseAttachment, Attachment, CourseGlossary, Glossary, CourseLiveSession, LiveSessionUser, LiveSessionRecording, Participant ) from apps.account.models import ProfessorUser from apps.course.models.course import extract_text_from_json, get_localized_field def get_request_lang(serializer_instance): request = serializer_instance.context.get('request') language_code = getattr(request, 'LANGUAGE_CODE', None) if language_code: return str(language_code).lower() header_value = '' if request is not None: if hasattr(request, 'headers'): header_value = request.headers.get('Accept-Language', '') if not header_value and hasattr(request, 'META'): header_value = request.META.get('HTTP_ACCEPT_LANGUAGE', '') if header_value: return header_value.split(',')[0].strip().lower() return 'en' class AdminLessonSerializer(serializers.ModelSerializer): title_i18n = serializers.JSONField(source='title', read_only=True) content_type_i18n = serializers.JSONField(source='content_type', read_only=True) class Meta: model = Lesson fields = [ 'id', 'title', 'title_i18n', 'content_type', 'content_type_i18n', 'content_file', 'video_link', 'duration', 'created_at', 'updated_at', ] def to_representation(self, instance): ret = super().to_representation(instance) lang = get_request_lang(self) ret['title'] = get_localized_field(lang, instance.title) ret['content_type'] = get_localized_field(lang, instance.content_type) return ret class AdminGlossarySerializer(serializers.ModelSerializer): description_i18n = serializers.JSONField(source='description', read_only=True) class Meta: model = Glossary fields = '__all__' def to_representation(self, instance): ret = super().to_representation(instance) lang = get_request_lang(self) ret['description'] = get_localized_field(lang, instance.description) return ret class AdminAttachmentSerializer(serializers.ModelSerializer): title_i18n = serializers.JSONField(source='title', read_only=True) class Meta: model = Attachment fields = '__all__' def to_representation(self, instance): ret = super().to_representation(instance) lang = get_request_lang(self) ret['title'] = get_localized_field(lang, instance.title) return ret def serialize_file(value, serializer): if not value: return None field = FileFieldSerializer() field.bind('', serializer) return field.to_representation(value) class AdminCourseCategorySerializer(serializers.ModelSerializer): slug = serializers.JSONField(required=False, allow_null=True) name_i18n = serializers.JSONField(source='name', read_only=True) slug_i18n = serializers.JSONField(source='slug', read_only=True) class Meta: model = CourseCategory fields = ['id', 'name', 'name_i18n', 'slug', 'slug_i18n', 'icon'] def to_representation(self, instance): ret = super().to_representation(instance) lang = get_request_lang(self) ret['name'] = get_localized_field(lang, instance.name) ret['slug'] = get_localized_field(lang, instance.slug) icon = ret.get('icon') if icon and icon.startswith('/'): request = self.context.get('request') if request: ret['icon'] = request.build_absolute_uri(icon) else: from django.conf import settings ret['icon'] = f"{settings.SITE_DOMAIN}{icon}" return ret class AdminCourseListSerializer(serializers.ModelSerializer): category_name = serializers.SerializerMethodField() professor_name = serializers.SerializerMethodField(read_only=True) professors = serializers.PrimaryKeyRelatedField(queryset=ProfessorUser.objects.all(), many=True, required=False) professor = serializers.PrimaryKeyRelatedField(queryset=ProfessorUser.objects.all(), required=False, allow_null=True, write_only=True) participant_count = serializers.SerializerMethodField() thumbnail = serializers.SerializerMethodField() class Meta: model = Course fields = [ 'id', 'title', 'slug', 'category', 'category_name', 'professor', 'professors', 'professor_name', 'thumbnail', 'is_online', 'level', 'duration', 'lessons_count', 'status', 'is_free', 'price', 'price_rub', 'discount_percentage', 'final_price', 'final_price_rub', 'participant_count', 'created_at' ] def get_professor_name(self, obj): return ", ".join([p.fullname for p in obj.professors.all()]) def get_participant_count(self, obj): return obj.participants.count() def get_thumbnail(self, obj): return get_thumbs(obj.thumbnail, self.context.get('request')) def get_category_name(self, obj): if not obj.category: return "" lang = get_request_lang(self) return get_localized_field(lang, obj.category.name) def to_representation(self, instance): ret = super().to_representation(instance) lang = get_request_lang(self) ret['title'] = get_localized_field(lang, instance.title) ret['slug'] = get_localized_field(lang, instance.slug) ret['level'] = get_localized_field(lang, instance.level) ret['status'] = get_localized_field(lang, instance.status) # Inject legacy fields first_prof = instance.professors.first() ret['professor'] = first_prof.id if first_prof else None ret['professor_name'] = first_prof.fullname if first_prof else "" ret['professors'] = [p.id for p in instance.professors.all()] return ret class AdminCourseDetailSerializer(serializers.ModelSerializer): slug = serializers.JSONField(required=False, allow_null=True) category_name = serializers.SerializerMethodField() professor_name = serializers.SerializerMethodField(read_only=True) professors = serializers.PrimaryKeyRelatedField(queryset=ProfessorUser.objects.all(), many=True, required=False) professor = serializers.PrimaryKeyRelatedField(queryset=ProfessorUser.objects.all(), required=False, allow_null=True, write_only=True) thumbnail = FileFieldSerializer(required=False, allow_null=True) video_file = FileFieldSerializer(required=False, allow_null=True) participant_count = serializers.SerializerMethodField() class Meta: model = Course fields = [ 'id', 'title', 'slug', 'category', 'category_name', 'professor', 'professors', 'professor_name', 'thumbnail', 'video_type', 'video_file', 'video_link', 'is_online', 'online_link', 'level', 'duration', 'lessons_count', 'description', 'short_description', 'status', 'is_free', 'price', 'price_rub', 'discount_percentage', 'final_price', 'final_price_rub', 'is_group_chat_locked', 'is_professor_chat_locked', 'timing', 'features', 'participant_count', 'created_at', 'updated_at' ] read_only_fields = ['id', 'final_price', 'final_price_rub', 'created_at', 'updated_at'] def get_professor_name(self, obj): return ", ".join([p.fullname for p in obj.professors.all()]) def get_participant_count(self, obj): return obj.participants.count() def get_category_name(self, obj): if not obj.category: return "" lang = get_request_lang(self) return get_localized_field(lang, obj.category.name) def create(self, validated_data): professor = validated_data.pop('professor', None) professors = validated_data.pop('professors', None) instance = super().create(validated_data) if professors is not None: instance.professors.set(professors) elif professor is not None: instance.professors.set([professor]) return instance def update(self, instance, validated_data): professor = validated_data.pop('professor', None) professors = validated_data.pop('professors', None) instance = super().update(instance, validated_data) if professors is not None: instance.professors.set(professors) elif professor is not None: instance.professors.set([professor]) return instance def to_representation(self, instance): ret = super().to_representation(instance) lang = get_request_lang(self) ret['title'] = get_localized_field(lang, instance.title) ret['slug'] = get_localized_field(lang, instance.slug) ret['level'] = get_localized_field(lang, instance.level) ret['status'] = get_localized_field(lang, instance.status) ret['description'] = get_localized_field(lang, instance.description) ret['short_description'] = get_localized_field(lang, instance.short_description) # Inject legacy fields first_prof = instance.professors.first() ret['professor'] = first_prof.id if first_prof else None ret['professor_name'] = first_prof.fullname if first_prof else "" ret['professors'] = [p.id for p in instance.professors.all()] return ret class AdminCourseChapterSerializer(serializers.ModelSerializer): title_i18n = serializers.JSONField(source='title', read_only=True) class Meta: model = CourseChapter fields = ['id', 'course', 'title', 'title_i18n', 'priority', 'is_active', 'created_at'] read_only_fields = ['id', 'created_at'] def to_representation(self, instance): ret = super().to_representation(instance) lang = get_request_lang(self) ret['title'] = get_localized_field(lang, instance.title) return ret class AdminCourseLessonSerializer(serializers.Serializer): id = serializers.IntegerField(read_only=True) course = serializers.IntegerField(required=False, read_only=True) chapter = serializers.IntegerField() lesson = serializers.IntegerField(required=False, write_only=True, allow_null=True) title = serializers.JSONField(required=False, allow_null=True) priority = serializers.IntegerField(required=False, allow_null=True) is_active = serializers.BooleanField(default=True) # Lesson (Base Content) fields content_type = serializers.JSONField(required=False, allow_null=True) content_file = FileFieldSerializer(required=False, allow_null=True) video_link = serializers.CharField(max_length=500, required=False, allow_blank=True, allow_null=True) duration = serializers.IntegerField(min_value=0, required=False, default=0) class Meta: ref_name = 'CourseAdminCourseLesson' def to_representation(self, instance): """ Map a CourseLesson instance to representation fields. """ lesson_obj = instance.lesson lang = get_request_lang(self) raw_title = instance.title or (lesson_obj.title if lesson_obj else []) localized_title = get_localized_field(lang, raw_title) raw_content_type = lesson_obj.content_type if lesson_obj else [] localized_content_type = get_localized_field(lang, raw_content_type) return { 'id': instance.id, 'course': instance.course_id, 'chapter': instance.chapter_id, 'lesson': instance.lesson_id, 'title': localized_title, 'title_i18n': raw_title, 'priority': instance.priority, 'is_active': instance.is_active, 'content_type': localized_content_type, 'content_type_i18n': raw_content_type, 'content_file': serialize_file(lesson_obj.content_file, self) if lesson_obj and lesson_obj.content_file else None, 'video_link': lesson_obj.video_link if lesson_obj else "", 'duration': lesson_obj.duration if lesson_obj else 0, } def create(self, validated_data): chapter_id = validated_data.pop('chapter') chapter = CourseChapter.objects.get(pk=chapter_id) course = chapter.course title = validated_data.get('title') priority = validated_data.get('priority') is_active = validated_data.get('is_active', True) lesson_id = validated_data.get('lesson') if lesson_id: try: lesson = Lesson.objects.get(pk=lesson_id) except Lesson.DoesNotExist: raise serializers.ValidationError({"lesson": "Lesson does not exist."}) # Check if already linked to this course if CourseLesson.objects.filter(course=course, lesson=lesson).exists(): raise serializers.ValidationError({"detail": "This lesson is already linked to this course."}) else: content_type = validated_data.get('content_type') if not content_type: raise serializers.ValidationError({"detail": "Content type is required for new lessons."}) content_file = validated_data.get('content_file') video_link = validated_data.get('video_link') duration = validated_data.get('duration', 0) # 1. Create Lesson Base lesson = Lesson.objects.create( title=title or "Base Lesson", content_type=content_type, content_file=content_file, video_link=video_link, duration=duration ) # 2. Create CourseLesson junction course_lesson = CourseLesson.objects.create( course=course, chapter=chapter, lesson=lesson, title=title, priority=priority, is_active=is_active ) return course_lesson def update(self, instance, validated_data): chapter_id = validated_data.get('chapter') if chapter_id: chapter = CourseChapter.objects.get(pk=chapter_id) instance.chapter = chapter instance.course = chapter.course if 'title' in validated_data: instance.title = validated_data['title'] if 'priority' in validated_data: instance.priority = validated_data['priority'] if 'is_active' in validated_data: instance.is_active = validated_data['is_active'] instance.save() # Update the Base Lesson as well lesson = instance.lesson if lesson: if 'title' in validated_data and validated_data['title']: lesson.title = validated_data['title'] if 'content_type' in validated_data: lesson.content_type = validated_data['content_type'] if 'content_file' in validated_data: lesson.content_file = validated_data['content_file'] if 'video_link' in validated_data: lesson.video_link = validated_data['video_link'] if 'duration' in validated_data: lesson.duration = validated_data['duration'] lesson.save() return instance class AdminCourseAttachmentSerializer(serializers.Serializer): id = serializers.IntegerField(read_only=True) course = serializers.IntegerField() attachment = serializers.IntegerField(required=False, write_only=True, allow_null=True) title = serializers.JSONField(required=False, allow_null=True) file = FileFieldSerializer(required=False, allow_null=True) file_size = serializers.IntegerField(read_only=True) def to_representation(self, instance): attachment_obj = instance.attachment lang = get_request_lang(self) raw_title = attachment_obj.title if attachment_obj else [] localized_title = get_localized_field(lang, raw_title) return { 'id': instance.id, 'course': instance.course_id, 'attachment': instance.attachment_id, 'title': localized_title, 'title_i18n': raw_title, 'file': serialize_file(attachment_obj.file, self) if attachment_obj and attachment_obj.file else None, 'file_size': attachment_obj.file_size if attachment_obj else 0 } def create(self, validated_data): course_id = validated_data.pop('course') course = Course.objects.get(pk=course_id) attachment_id = validated_data.get('attachment') if attachment_id: try: attachment = Attachment.objects.get(pk=attachment_id) except Attachment.DoesNotExist: raise serializers.ValidationError({"attachment": "Attachment does not exist."}) # Check if already linked if CourseAttachment.objects.filter(course=course, attachment=attachment).exists(): raise serializers.ValidationError({"detail": "This attachment is already linked to this course."}) else: title = validated_data.get('title') file_data = validated_data.get('file') if not title or not file_data: raise serializers.ValidationError({"detail": "Title and file are required for new attachments."}) # 1. Create Base Attachment attachment = Attachment.objects.create( title=title, file=file_data ) # 2. Link to Course course_attachment = CourseAttachment.objects.create( course=course, attachment=attachment ) return course_attachment def update(self, instance, validated_data): # We don't change the course in updates, just details of attachment attachment = instance.attachment if attachment: if 'title' in validated_data: attachment.title = validated_data['title'] if 'file' in validated_data: attachment.file = validated_data['file'] # Reset file size to trigger recalculation in save() attachment.file_size = None attachment.save() return instance class AdminCourseGlossarySerializer(serializers.Serializer): id = serializers.IntegerField(read_only=True) course = serializers.IntegerField() glossary = serializers.IntegerField(required=False, write_only=True, allow_null=True) title = serializers.CharField(max_length=555, required=False, allow_blank=True, allow_null=True) description = serializers.JSONField(required=False, allow_null=True) def to_representation(self, instance): glossary_obj = instance.glossary lang = get_request_lang(self) raw_description = glossary_obj.description if glossary_obj else [] localized_description = get_localized_field(lang, raw_description) return { 'id': instance.id, 'course': instance.course_id, 'glossary': instance.glossary_id, 'title': glossary_obj.title if glossary_obj else "", 'description': localized_description, 'description_i18n': raw_description, } def create(self, validated_data): course_id = validated_data.pop('course') course = Course.objects.get(pk=course_id) glossary_id = validated_data.get('glossary') if glossary_id: try: glossary = Glossary.objects.get(pk=glossary_id) except Glossary.DoesNotExist: raise serializers.ValidationError({"glossary": "Glossary term does not exist."}) # Check if already linked if CourseGlossary.objects.filter(course=course, glossary=glossary).exists(): raise serializers.ValidationError({"detail": "This glossary term is already linked to this course."}) else: title = validated_data.get('title') description = validated_data.get('description') if not title or not description: raise serializers.ValidationError({"detail": "Title and description are required for new glossary terms."}) # 1. Create Base Glossary glossary = Glossary.objects.create( title=title, description=description ) # 2. Link to Course course_glossary = CourseGlossary.objects.create( course=course, glossary=glossary ) return course_glossary def update(self, instance, validated_data): glossary = instance.glossary if glossary: if 'title' in validated_data: glossary.title = validated_data['title'] if 'description' in validated_data: glossary.description = validated_data['description'] glossary.save() return instance class AdminLiveSessionUserSerializer(serializers.ModelSerializer): user_name = serializers.CharField(source='user.fullname', read_only=True) user_email = serializers.CharField(source='user.email', read_only=True) class Meta: model = LiveSessionUser fields = ['id', 'session', 'user', 'user_name', 'user_email', 'role', 'entered_at', 'exited_at', 'is_online'] class AdminLiveSessionRecordingSerializer(serializers.ModelSerializer): file = FileFieldSerializer(required=False, allow_null=True) thumbnail = FileFieldSerializer(required=False, allow_null=True) class Meta: model = LiveSessionRecording fields = ['id', 'session', 'title', 'file', 'file_time', 'recording_type', 'thumbnail', 'is_active', 'created_at'] class AdminLiveSessionListSerializer(serializers.ModelSerializer): course_title = serializers.SerializerMethodField() participant_count = serializers.SerializerMethodField() media_count = serializers.IntegerField(read_only=True) class Meta: model = CourseLiveSession fields = ['id', 'course', 'course_title', 'room_id', 'subject', 'started_at', 'ended_at', 'participant_count', 'media_count', 'created_at'] def get_course_title(self, obj): if not obj.course: return "" lang = get_request_lang(self) return get_localized_field(lang, obj.course.title) def get_participant_count(self, obj): return obj.user_sessions.count() class AdminLiveSessionDetailSerializer(serializers.ModelSerializer): course_title = serializers.SerializerMethodField() user_sessions = serializers.SerializerMethodField() recordings = AdminLiveSessionRecordingSerializer(many=True, read_only=True) recorded_file = FileFieldSerializer(required=False, allow_null=True) class Meta: model = CourseLiveSession fields = ['id', 'course', 'course_title', 'room_id', 'subject', 'started_at', 'ended_at', 'recorded_file', 'user_sessions', 'recordings', 'created_at', 'updated_at'] def get_course_title(self, obj): if not obj.course: return "" lang = get_request_lang(self) return get_localized_field(lang, obj.course.title) def get_user_sessions(self, obj): # 1. Get all active participants of this course participants = obj.course.participants.filter(is_active=True).select_related('student') # 2. Get all live session user records for this session attendance_map = { ls_user.user_id: ls_user for ls_user in obj.user_sessions.all().select_related('user') } # 3. Build the combined list data = [] for p in participants: student = p.student # Check if this student has an attendance record attendance = attendance_map.get(student.id) if attendance: data.append({ 'id': attendance.id, 'session': obj.id, 'user': student.id, 'user_name': student.fullname or student.username, 'user_email': student.email, 'role': attendance.role, 'entered_at': attendance.entered_at.isoformat() if attendance.entered_at else None, 'exited_at': attendance.exited_at.isoformat() if attendance.exited_at else None, 'is_online': attendance.is_online, 'has_attended': True }) else: data.append({ 'id': None, 'session': obj.id, 'user': student.id, 'user_name': student.fullname or student.username, 'user_email': student.email, 'role': 'participant', 'entered_at': None, 'exited_at': None, 'is_online': False, 'has_attended': False }) # Also append any attendees who are NOT in the participants list (e.g. professor/moderator/observer) participant_student_ids = {p.student_id for p in participants} for user_id, attendance in attendance_map.items(): if user_id not in participant_student_ids: data.append({ 'id': attendance.id, 'session': obj.id, 'user': user_id, 'user_name': attendance.user.fullname or attendance.user.username, 'user_email': attendance.user.email, 'role': attendance.role, 'entered_at': attendance.entered_at.isoformat() if attendance.entered_at else None, 'exited_at': attendance.exited_at.isoformat() if attendance.exited_at else None, 'is_online': attendance.is_online, 'has_attended': True }) return data