diff --git a/apps/api/views/admin_dashboard.py b/apps/api/views/admin_dashboard.py index 3a6f70d..06df6b6 100644 --- a/apps/api/views/admin_dashboard.py +++ b/apps/api/views/admin_dashboard.py @@ -8,6 +8,7 @@ from django.db.models.functions import TruncDate from apps.account.models import User, LoginHistory from apps.course.models import Course, Participant, CourseLiveSession +from apps.course.models.course import extract_text_from_json from apps.quiz.models import QuizParticipant from apps.blog.models import Blog from apps.chat.models import ChatMessage, RoomMessage @@ -31,7 +32,7 @@ class AdminDashboardStatsView(APIView): is_active=True ).count() professors = User.objects.filter(user_type=User.UserType.PROFESSOR, is_active=True).count() - active_courses = Course.objects.filter(status=Course.StatusChoices.ONGOING).count() + active_courses = Course.objects.filter(status__contains=[{"title": Course.StatusChoices.ONGOING}]).count() total_blogs = Blog.objects.count() pending_certificates = Certificate.objects.filter(status='pending').count() active_live_sessions = CourseLiveSession.objects.filter(ended_at__isnull=True).count() @@ -84,10 +85,9 @@ class AdminDashboardStatsView(APIView): chat_chart = [{"date": item['date'].strftime('%Y-%m-%d'), "messages": item['count']} for item in chat_volume] # Course Pipeline (Stacked Bar) - course_statuses = Course.objects.values('status').annotate(count=Count('id')) course_pipeline = {"name": "Courses"} - for status in course_statuses: - course_pipeline[status['status']] = status['count'] + for choice in Course.StatusChoices.values: + course_pipeline[choice] = Course.objects.filter(status__contains=[{"title": choice}]).count() # Course Efficacy: Completed vs Enrolled (Bar Chart) # We take the top 5 courses, check how many users are enrolled, and how many have at least 1 completed lesson @@ -99,8 +99,9 @@ class AdminDashboardStatsView(APIView): course=course, student__lesson_completions__course_lesson__course=course ).distinct().count() + title_text = extract_text_from_json(course.title) course_efficacy.append({ - "title": course.title[:20] + "..." if len(course.title) > 20 else course.title, + "title": title_text[:20] + "..." if len(title_text) > 20 else title_text, "enrolled": enrolled, "completed": completed }) @@ -113,7 +114,7 @@ class AdminDashboardStatsView(APIView): top_courses_list = [ { "id": c.id, - "title": c.title, + "title": extract_text_from_json(c.title), "professor": c.professor.fullname if c.professor else "Unknown", "participants": c.participant_count } for c in top_courses_qs diff --git a/apps/certificate/serializers.py b/apps/certificate/serializers.py index a8d405d..cce380b 100644 --- a/apps/certificate/serializers.py +++ b/apps/certificate/serializers.py @@ -50,7 +50,7 @@ class CertificateRequestSerializer(serializers.ModelSerializer): class AdminCertificateSerializer(serializers.ModelSerializer): student_name = serializers.CharField(source='student.fullname', read_only=True) student_email = serializers.CharField(source='student.email', read_only=True) - course_title = serializers.CharField(source='course.title', read_only=True) + course_title = serializers.SerializerMethodField() class Meta: model = Certificate @@ -60,6 +60,14 @@ class AdminCertificateSerializer(serializers.ModelSerializer): 'created_at', 'updated_at' ] + def get_course_title(self, obj): + if not obj.course: + return None + request = self.context.get('request') + lang = getattr(request, 'LANGUAGE_CODE', None) or 'en' + from apps.course.models.course import get_localized_field + return get_localized_field(lang, obj.course.title) + diff --git a/apps/course/serializers/admin.py b/apps/course/serializers/admin.py index 5474599..3603fc0 100644 --- a/apps/course/serializers/admin.py +++ b/apps/course/serializers/admin.py @@ -15,23 +15,46 @@ from apps.course.models import ( LiveSessionRecording ) from apps.account.models import ProfessorUser -from apps.course.models.course import extract_text_from_json +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') + return getattr(request, 'LANGUAGE_CODE', None) or 'en' class AdminLessonSerializer(serializers.ModelSerializer): class Meta: model = Lesson 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) + ret['content_type'] = get_localized_field(lang, instance.content_type) + return ret + class AdminGlossarySerializer(serializers.ModelSerializer): 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): 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: @@ -47,6 +70,13 @@ class AdminCourseCategorySerializer(serializers.ModelSerializer): model = CourseCategory fields = ['id', 'name', 'slug'] + 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) + return ret + class AdminCourseListSerializer(serializers.ModelSerializer): category_name = serializers.SerializerMethodField() professor_name = serializers.CharField(source='professor.fullname', read_only=True) @@ -84,7 +114,19 @@ class AdminCourseListSerializer(serializers.ModelSerializer): return get_thumbs(obj.thumbnail, self.context.get('request')) def get_category_name(self, obj): - return extract_text_from_json(obj.category.name) if obj.category else "" + 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) + return ret class AdminCourseDetailSerializer(serializers.ModelSerializer): @@ -135,7 +177,21 @@ class AdminCourseDetailSerializer(serializers.ModelSerializer): return obj.participants.count() def get_category_name(self, obj): - return extract_text_from_json(obj.category.name) if obj.category else "" + 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) + ret['timing'] = get_localized_field(lang, instance.timing) + ret['features'] = get_localized_field(lang, instance.features) + return ret class AdminCourseChapterSerializer(serializers.ModelSerializer): @@ -144,6 +200,12 @@ class AdminCourseChapterSerializer(serializers.ModelSerializer): fields = ['id', 'course', 'title', '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) @@ -168,15 +230,23 @@ class AdminCourseLessonSerializer(serializers.Serializer): 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': instance.title or (lesson_obj.title if lesson_obj else []), + 'title': localized_title, 'priority': instance.priority, 'is_active': instance.is_active, - 'content_type': lesson_obj.content_type if lesson_obj else [], + 'content_type': localized_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, @@ -273,11 +343,14 @@ class AdminCourseAttachmentSerializer(serializers.Serializer): 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': attachment_obj.title if attachment_obj else [], + 'title': localized_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 } @@ -339,12 +412,15 @@ class AdminCourseGlossarySerializer(serializers.Serializer): 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': glossary_obj.description if glossary_obj else [] + 'description': localized_description } def create(self, validated_data): @@ -411,19 +487,25 @@ class AdminLiveSessionRecordingSerializer(serializers.ModelSerializer): class AdminLiveSessionListSerializer(serializers.ModelSerializer): - course_title = serializers.CharField(source='course.title', read_only=True) + course_title = serializers.SerializerMethodField() participant_count = serializers.SerializerMethodField() class Meta: model = CourseLiveSession fields = ['id', 'course', 'course_title', 'room_id', 'subject', 'started_at', 'ended_at', 'participant_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.CharField(source='course.title', read_only=True) + course_title = serializers.SerializerMethodField() user_sessions = AdminLiveSessionUserSerializer(many=True, read_only=True) recordings = AdminLiveSessionRecordingSerializer(many=True, read_only=True) recorded_file = FileFieldSerializer(required=False, allow_null=True) @@ -431,3 +513,9 @@ class AdminLiveSessionDetailSerializer(serializers.ModelSerializer): 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) diff --git a/apps/course/views/course.py b/apps/course/views/course.py index 66a31c7..f4e39ef 100644 --- a/apps/course/views/course.py +++ b/apps/course/views/course.py @@ -37,6 +37,7 @@ from apps.course.models import ( LiveSessionUser, Participant, ) +from apps.course.models.course import extract_text_from_json from apps.course.doc import * from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError from apps.account.serializers import UserProfileSerializer @@ -618,7 +619,7 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): }, status=status.HTTP_200_OK) def _build_metadata(self, course: Course, payload: dict, user=None) -> dict: - status_value = course.status + status_value = extract_text_from_json(course.status) has_started = status_value in [Course.StatusChoices.ONGOING, Course.StatusChoices.FINISHED] timing_data = course.timing if isinstance(course.timing, dict) else {} diff --git a/apps/course/views/professor.py b/apps/course/views/professor.py index 2cdf251..ee514d6 100644 --- a/apps/course/views/professor.py +++ b/apps/course/views/professor.py @@ -202,4 +202,4 @@ class ProfessorCourseListAPIView(ListAPIView): 'lessons__lesson', 'lessons__completions', 'participants__student', - ).exclude(status=Course.StatusChoices.INACTIVE).filter(professor=professor) + ).exclude(status__contains=[{"title": Course.StatusChoices.INACTIVE}]).filter(professor=professor) diff --git a/apps/quiz/serializers/admin.py b/apps/quiz/serializers/admin.py index 86a8a1e..c5c21d2 100644 --- a/apps/quiz/serializers/admin.py +++ b/apps/quiz/serializers/admin.py @@ -2,6 +2,11 @@ from rest_framework import serializers from apps.quiz.models import Quiz, Question, QuizParticipant, ParticipantAnswer from apps.course.models import Course, CourseLesson from apps.account.models import User +from apps.course.models.course import get_localized_field + +def get_request_lang(serializer_instance): + request = serializer_instance.context.get('request') + return getattr(request, 'LANGUAGE_CODE', None) or 'en' class AdminCourseSerializer(serializers.ModelSerializer): @@ -9,6 +14,12 @@ class AdminCourseSerializer(serializers.ModelSerializer): model = Course fields = ['id', 'title'] + 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.ModelSerializer): title = serializers.SerializerMethodField() @@ -19,11 +30,9 @@ class AdminCourseLessonSerializer(serializers.ModelSerializer): ref_name = 'QuizAdminCourseLesson' def get_title(self, obj): - if obj.title: - return obj.title - if obj.lesson: - return obj.lesson.title - return f"Lesson {obj.id}" + lang = get_request_lang(self) + raw_title = obj.title or (obj.lesson.title if obj.lesson else []) + return get_localized_field(lang, raw_title) class AdminQuizListSerializer(serializers.ModelSerializer): @@ -55,12 +64,17 @@ class AdminQuizListSerializer(serializers.ModelSerializer): return obj.participants.count() def get_course_title(self, obj): - return obj.course.title if obj.course else None + if not obj.course: + return None + lang = get_request_lang(self) + return get_localized_field(lang, obj.course.title) def get_lesson_title(self, obj): if not obj.lesson: return None - return obj.lesson.title or (obj.lesson.lesson.title if obj.lesson.lesson else None) + lang = get_request_lang(self) + raw_title = obj.lesson.title or (obj.lesson.lesson.title if obj.lesson.lesson else None) + return get_localized_field(lang, raw_title) class AdminQuizDetailSerializer(serializers.ModelSerializer): @@ -93,12 +107,17 @@ class AdminQuizDetailSerializer(serializers.ModelSerializer): return obj.participants.count() def get_course_title(self, obj): - return obj.course.title if obj.course else None + if not obj.course: + return None + lang = get_request_lang(self) + return get_localized_field(lang, obj.course.title) def get_lesson_title(self, obj): if not obj.lesson: return None - return obj.lesson.title or (obj.lesson.lesson.title if obj.lesson.lesson else None) + lang = get_request_lang(self) + raw_title = obj.lesson.title or (obj.lesson.lesson.title if obj.lesson.lesson else None) + return get_localized_field(lang, raw_title) class AdminQuestionSerializer(serializers.ModelSerializer): diff --git a/apps/transaction/serializers.py b/apps/transaction/serializers.py index e243f1b..1e49e32 100644 --- a/apps/transaction/serializers.py +++ b/apps/transaction/serializers.py @@ -127,7 +127,7 @@ class AdminParticipantInfoSerializer(serializers.ModelSerializer): class AdminTransactionSerializer(serializers.ModelSerializer): user_name = serializers.CharField(source='user.fullname', read_only=True) user_email = serializers.CharField(source='user.email', read_only=True) - course_title = serializers.CharField(source='course.title', read_only=True) + course_title = serializers.SerializerMethodField() receipts = AdminTransactionReceiptSerializer(many=True, read_only=True) participant_infos = AdminParticipantInfoSerializer(many=True, read_only=True) @@ -144,4 +144,12 @@ class AdminTransactionSerializer(serializers.ModelSerializer): 'id', 'user', 'user_name', 'user_email', 'course', 'course_title', 'price', 'payment_method', 'participant_infos', 'receipts', 'created_at', 'updated_at' - ] \ No newline at end of file + ] + + def get_course_title(self, obj): + if not obj.course: + return None + request = self.context.get('request') + lang = getattr(request, 'LANGUAGE_CODE', None) or 'en' + from apps.course.models.course import get_localized_field + return get_localized_field(lang, obj.course.title) \ No newline at end of file