Browse Source
course admin endpoints added
course admin endpoints added
created views and serializers for CRUD actions and filtering and ... for course page of admin panelmaster
6 changed files with 487 additions and 4 deletions
-
315apps/course/serializers/admin.py
-
2apps/course/serializers/course.py
-
12apps/course/urls.py
-
1apps/course/views/__init__.py
-
160apps/course/views/admin.py
-
1config/urls.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 |
||||
@ -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() |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue