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