From edadae54e3f0bd645553d08731977bb030ae81b8 Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Tue, 26 May 2026 12:10:24 +0330 Subject: [PATCH] course admin endpoints added created views and serializers for CRUD actions and filtering and ... for course page of admin panel --- apps/course/serializers/admin.py | 315 ++++++++++++++++++++++++++++++ apps/course/serializers/course.py | 2 +- apps/course/urls.py | 12 +- apps/course/views/__init__.py | 1 + apps/course/views/admin.py | 160 +++++++++++++++ config/urls.py | 1 + 6 files changed, 487 insertions(+), 4 deletions(-) create mode 100644 apps/course/serializers/admin.py create mode 100644 apps/course/views/admin.py diff --git a/apps/course/serializers/admin.py b/apps/course/serializers/admin.py new file mode 100644 index 0000000..59d9e37 --- /dev/null +++ b/apps/course/serializers/admin.py @@ -0,0 +1,315 @@ +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 +) +from apps.account.models import ProfessorUser + +def serialize_file(value, serializer): + if not value: + return None + field = FileFieldSerializer() + field.bind('', serializer) + return field.to_representation(value) + +class AdminCourseCategorySerializer(serializers.ModelSerializer): + class Meta: + model = CourseCategory + fields = ['id', 'name', 'slug'] + +class AdminCourseListSerializer(serializers.ModelSerializer): + category_name = serializers.CharField(source='category.name', read_only=True) + professor_name = serializers.CharField(source='professor.fullname', read_only=True) + participant_count = serializers.SerializerMethodField() + thumbnail = serializers.SerializerMethodField() + + class Meta: + model = Course + fields = [ + 'id', + 'title', + 'slug', + 'category', + 'category_name', + 'professor', + 'professor_name', + 'thumbnail', + 'is_online', + 'level', + 'duration', + 'lessons_count', + 'status', + 'is_free', + 'price', + 'discount_percentage', + 'final_price', + 'participant_count', + 'created_at' + ] + + def get_participant_count(self, obj): + return obj.participants.count() + + def get_thumbnail(self, obj): + return get_thumbs(obj.thumbnail, self.context.get('request')) + + +class AdminCourseDetailSerializer(serializers.ModelSerializer): + category_name = serializers.CharField(source='category.name', read_only=True) + professor_name = serializers.CharField(source='professor.fullname', read_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', + 'professor_name', + 'thumbnail', + 'video_type', + 'video_file', + 'video_link', + 'is_online', + 'online_link', + 'level', + 'duration', + 'lessons_count', + 'description', + 'short_description', + 'status', + 'is_free', + 'price', + 'discount_percentage', + 'final_price', + 'is_group_chat_locked', + 'is_professor_chat_locked', + 'timing', + 'features', + 'participant_count', + 'created_at', + 'updated_at' + ] + read_only_fields = ['id', 'slug', 'final_price', 'created_at', 'updated_at'] + + def get_participant_count(self, obj): + return obj.participants.count() + + +class AdminCourseChapterSerializer(serializers.ModelSerializer): + class Meta: + model = CourseChapter + fields = ['id', 'course', 'title', 'priority', 'is_active', 'created_at'] + read_only_fields = ['id', 'created_at'] + + +class AdminCourseLessonSerializer(serializers.Serializer): + id = serializers.IntegerField(read_only=True) + course = serializers.IntegerField(required=False, read_only=True) + chapter = serializers.IntegerField() + title = serializers.CharField(max_length=255, required=False, allow_blank=True, allow_null=True) + priority = serializers.IntegerField(required=False, allow_null=True) + is_active = serializers.BooleanField(default=True) + + # Lesson (Base Content) fields + content_type = serializers.ChoiceField(choices=Lesson.ContentTypeChoices.choices) + 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) + + def to_representation(self, instance): + """ + Map a CourseLesson instance to representation fields. + """ + lesson_obj = instance.lesson + return { + 'id': instance.id, + 'course': instance.course_id, + 'chapter': instance.chapter_id, + 'title': instance.title or (lesson_obj.title if lesson_obj else ""), + 'priority': instance.priority, + 'is_active': instance.is_active, + 'content_type': lesson_obj.content_type if lesson_obj else None, + '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) + + content_type = validated_data.get('content_type') + 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() + title = serializers.CharField(max_length=255) + file = FileFieldSerializer(required=True) + file_size = serializers.IntegerField(read_only=True) + + def to_representation(self, instance): + attachment_obj = instance.attachment + return { + 'id': instance.id, + 'course': instance.course_id, + 'title': attachment_obj.title if attachment_obj else "", + '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) + + title = validated_data.get('title') + file_data = validated_data.get('file') + + # 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() + title = serializers.CharField(max_length=555) + description = serializers.CharField() + + def to_representation(self, instance): + glossary_obj = instance.glossary + return { + 'id': instance.id, + 'course': instance.course_id, + 'title': glossary_obj.title if glossary_obj else "", + 'description': glossary_obj.description if glossary_obj else "" + } + + def create(self, validated_data): + course_id = validated_data.pop('course') + course = Course.objects.get(pk=course_id) + + title = validated_data.get('title') + description = validated_data.get('description') + + # 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 diff --git a/apps/course/serializers/course.py b/apps/course/serializers/course.py index 30df8b6..d6ecaa1 100644 --- a/apps/course/serializers/course.py +++ b/apps/course/serializers/course.py @@ -12,7 +12,7 @@ class CourseCategorySerializer(serializers.ModelSerializer): class Meta: model = CourseCategory - fields = ['name', 'slug', 'course_count'] + fields = ['id', 'name', 'slug', 'course_count'] def get_course_count(self, obj): return obj.course_count diff --git a/apps/course/urls.py b/apps/course/urls.py index 2ef331a..0419894 100644 --- a/apps/course/urls.py +++ b/apps/course/urls.py @@ -1,11 +1,17 @@ -from django.urls import path, re_path - +from django.urls import path, re_path, include +from rest_framework.routers import DefaultRouter from . import views - +router = DefaultRouter() +router.register(r'admin/courses', views.AdminCourseViewSet, basename='admin-courses') +router.register(r'admin/chapters', views.AdminChapterViewSet, basename='admin-chapters') +router.register(r'admin/lessons', views.AdminCourseLessonViewSet, basename='admin-lessons') +router.register(r'admin/attachments', views.AdminCourseAttachmentViewSet, basename='admin-attachments') +router.register(r'admin/glossaries', views.AdminCourseGlossaryViewSet, basename='admin-glossaries') urlpatterns = [ + path('', include(router.urls)), path('categories/', views.CourseCategoryAPIView.as_view(), name='course-categories'), path('', views.CourseListAPIView.as_view(), name='course-list'), path('my-courses/', views.MyCourseListAPIView.as_view(), name='course-my-courses-list'), diff --git a/apps/course/views/__init__.py b/apps/course/views/__init__.py index 70ef373..6fc4005 100644 --- a/apps/course/views/__init__.py +++ b/apps/course/views/__init__.py @@ -4,3 +4,4 @@ from .participant import * from .professor import * from .live_session import * from .webhook import * +from .admin import * diff --git a/apps/course/views/admin.py b/apps/course/views/admin.py new file mode 100644 index 0000000..c392a71 --- /dev/null +++ b/apps/course/views/admin.py @@ -0,0 +1,160 @@ +from django.db.models import Q +from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated, IsAdminUser +from rest_framework.authentication import TokenAuthentication +from apps.course.models import ( + Course, + CourseChapter, + CourseLesson, + CourseAttachment, + CourseGlossary +) +from apps.course.serializers.admin import ( + AdminCourseListSerializer, + AdminCourseDetailSerializer, + AdminCourseChapterSerializer, + AdminCourseLessonSerializer, + AdminCourseAttachmentSerializer, + AdminCourseGlossarySerializer +) + +class AdminCourseViewSet(viewsets.ModelViewSet): + """ + Admin CRUD ViewSet for Course management. + """ + permission_classes = [IsAuthenticated, IsAdminUser] + authentication_classes = [TokenAuthentication] + + def get_serializer_class(self): + if self.action == 'list': + return AdminCourseListSerializer + return AdminCourseDetailSerializer + + def get_queryset(self): + queryset = Course.objects.all().select_related('category', 'professor') + + # Search Query + search_query = self.request.query_params.get('search', None) + if search_query: + queryset = queryset.filter( + Q(title__icontains=search_query) | + Q(description__icontains=search_query) | + Q(short_description__icontains=search_query) + ) + + # Filters + category_id = self.request.query_params.get('category', None) + if category_id: + queryset = queryset.filter(category_id=category_id) + + level = self.request.query_params.get('level', None) + if level: + queryset = queryset.filter(level=level) + + status_param = self.request.query_params.get('status', None) + if status_param: + queryset = queryset.filter(status=status_param) + + is_free_param = self.request.query_params.get('is_free', None) + if is_free_param is not None: + if is_free_param.lower() == 'true': + queryset = queryset.filter(is_free=True) + elif is_free_param.lower() == 'false': + queryset = queryset.filter(is_free=False) + + return queryset.order_by('-id') + + +class AdminChapterViewSet(viewsets.ModelViewSet): + """ + Admin CRUD ViewSet for Course Chapters. + """ + permission_classes = [IsAuthenticated, IsAdminUser] + authentication_classes = [TokenAuthentication] + serializer_class = AdminCourseChapterSerializer + pagination_class = None + + def get_queryset(self): + queryset = CourseChapter.objects.all() + course_id = self.request.query_params.get('course', None) + if course_id: + queryset = queryset.filter(course_id=course_id) + return queryset.order_by('priority', 'id') + + +class AdminCourseLessonViewSet(viewsets.ModelViewSet): + """ + Admin CRUD ViewSet for Lessons associated with a Course/Chapter. + """ + permission_classes = [IsAuthenticated, IsAdminUser] + authentication_classes = [TokenAuthentication] + serializer_class = AdminCourseLessonSerializer + pagination_class = None + + def get_queryset(self): + queryset = CourseLesson.objects.all().select_related('lesson', 'chapter') + + course_id = self.request.query_params.get('course', None) + if course_id: + queryset = queryset.filter(course_id=course_id) + + chapter_id = self.request.query_params.get('chapter', None) + if chapter_id: + queryset = queryset.filter(chapter_id=chapter_id) + + return queryset.order_by('priority', 'id') + + def perform_destroy(self, instance): + # Prevent orphans: delete the base Lesson object along with the junction model + lesson = instance.lesson + instance.delete() + if lesson: + lesson.delete() + + +class AdminCourseAttachmentViewSet(viewsets.ModelViewSet): + """ + Admin CRUD ViewSet for Course Attachments. + """ + permission_classes = [IsAuthenticated, IsAdminUser] + authentication_classes = [TokenAuthentication] + serializer_class = AdminCourseAttachmentSerializer + pagination_class = None + + def get_queryset(self): + queryset = CourseAttachment.objects.all().select_related('attachment') + course_id = self.request.query_params.get('course', None) + if course_id: + queryset = queryset.filter(course_id=course_id) + return queryset.order_by('-id') + + def perform_destroy(self, instance): + # Prevent orphans: delete base Attachment along with junction model + attachment = instance.attachment + instance.delete() + if attachment: + attachment.delete() + + +class AdminCourseGlossaryViewSet(viewsets.ModelViewSet): + """ + Admin CRUD ViewSet for Course Glossary terms. + """ + permission_classes = [IsAuthenticated, IsAdminUser] + authentication_classes = [TokenAuthentication] + serializer_class = AdminCourseGlossarySerializer + pagination_class = None + + def get_queryset(self): + queryset = CourseGlossary.objects.all().select_related('glossary') + course_id = self.request.query_params.get('course', None) + if course_id: + queryset = queryset.filter(course_id=course_id) + return queryset.order_by('-id') + + def perform_destroy(self, instance): + # Prevent orphans: delete base Glossary along with junction model + glossary = instance.glossary + instance.delete() + if glossary: + glossary.delete() diff --git a/config/urls.py b/config/urls.py index a66020b..105860d 100644 --- a/config/urls.py +++ b/config/urls.py @@ -129,3 +129,4 @@ if settings.DEBUG: +# Force reload trigger