You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
681 lines
26 KiB
681 lines
26 KiB
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.filter(is_active=True), many=True, required=False)
|
|
professor = serializers.PrimaryKeyRelatedField(queryset=ProfessorUser.objects.filter(is_active=True), 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.filter(is_active=True), many=True, required=False)
|
|
professor = serializers.PrimaryKeyRelatedField(queryset=ProfessorUser.objects.filter(is_active=True), 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
|