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.
379 lines
16 KiB
379 lines
16 KiB
from rest_framework.generics import ListAPIView, RetrieveAPIView, GenericAPIView
|
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
|
from rest_framework.authentication import TokenAuthentication
|
|
|
|
from drf_yasg.utils import swagger_auto_schema
|
|
from drf_yasg import openapi
|
|
from django.shortcuts import get_object_or_404
|
|
from rest_framework import status
|
|
from rest_framework.response import Response
|
|
from apps.course.models import CourseChapter
|
|
from apps.course.serializers import (
|
|
CourseLessonSerializer,
|
|
CourseChapterSerializer
|
|
)
|
|
from apps.course.models import Course, CourseLesson, LessonCompletion
|
|
from apps.course.access import user_has_course_access
|
|
from apps.course.doc import *
|
|
from utils.exceptions import AppAPIException
|
|
from utils.pagination import StandardResultsSetPagination
|
|
from rest_framework.permissions import IsAuthenticated
|
|
|
|
|
|
|
|
class LessonListView(ListAPIView):
|
|
serializer_class = CourseLessonSerializer
|
|
pagination_class = StandardResultsSetPagination
|
|
permission_classes = [AllowAny]
|
|
authentication_classes = [TokenAuthentication]
|
|
|
|
@swagger_auto_schema(
|
|
operation_description=doc_courses_lesson(),
|
|
tags=['Imam-Javad - Course'],
|
|
)
|
|
def get(self, request, *args, **kwargs):
|
|
return super().get(request, *args, **kwargs)
|
|
|
|
def get_queryset(self):
|
|
"""
|
|
Optimized queryset with select_related and prefetch_related for lesson relationships
|
|
"""
|
|
course_slug = self.kwargs.get('slug')
|
|
course = get_object_or_404(Course, slug__contains=[{"title": course_slug}])
|
|
|
|
return CourseLesson.objects.select_related(
|
|
'chapter', # 👇 Route through chapter
|
|
'lesson'
|
|
).prefetch_related(
|
|
'completions',
|
|
'quizzes'
|
|
).filter(
|
|
chapter__course=course, # 👇 Query via chapter
|
|
is_active=True,
|
|
chapter__is_active=True
|
|
).order_by('chapter__priority', 'priority') # 👇 Sort by chapter first
|
|
|
|
|
|
|
|
|
|
class LessonDetailView(RetrieveAPIView):
|
|
serializer_class = CourseLessonSerializer
|
|
permission_classes = [AllowAny]
|
|
authentication_classes = [TokenAuthentication]
|
|
|
|
@swagger_auto_schema(
|
|
operation_description="Get detailed lesson information with navigation data",
|
|
tags=["Imam-Javad - Course"],
|
|
manual_parameters=[
|
|
openapi.Parameter(
|
|
'id', openapi.IN_PATH,
|
|
description="Lesson ID",
|
|
type=openapi.TYPE_INTEGER,
|
|
required=True
|
|
)
|
|
],
|
|
responses={
|
|
200: openapi.Response(
|
|
description="Lesson details with navigation information",
|
|
schema=CourseLessonSerializer()
|
|
)
|
|
}
|
|
)
|
|
def get(self, request, *args, **kwargs):
|
|
lesson_id = self.kwargs.get('id')
|
|
course_lesson = get_object_or_404(
|
|
CourseLesson.objects.select_related('chapter__course', 'lesson'),
|
|
id=lesson_id,
|
|
is_active=True,
|
|
chapter__is_active=True
|
|
)
|
|
|
|
course = course_lesson.chapter.course
|
|
|
|
# Get all lessons in order
|
|
lessons = CourseLesson.objects.select_related(
|
|
'lesson', 'chapter'
|
|
).filter(
|
|
chapter__course=course,
|
|
is_active=True,
|
|
chapter__is_active=True
|
|
).order_by('chapter__priority', 'priority')
|
|
|
|
total_lessons = lessons.count()
|
|
# Convert to list to find index
|
|
lesson_ids = list(lessons.values_list('id', flat=True))
|
|
try:
|
|
current_index = lesson_ids.index(course_lesson.id)
|
|
current_lesson_number = current_index + 1
|
|
|
|
next_lesson_id = lesson_ids[current_index + 1] if current_index + 1 < len(lesson_ids) else None
|
|
previous_lesson_id = lesson_ids[current_index - 1] if current_index > 0 else None
|
|
|
|
except ValueError:
|
|
current_lesson_number = 1
|
|
next_lesson_id = None
|
|
previous_lesson_id = None
|
|
|
|
lesson_data = self.get_serializer(course_lesson).data
|
|
lesson_data['total_lessons'] = total_lessons
|
|
lesson_data['current_lesson_number'] = current_lesson_number
|
|
lesson_data['next_lesson_id'] = next_lesson_id
|
|
lesson_data['previous_lesson_id'] = previous_lesson_id
|
|
lesson_data['can_go_next'] = next_lesson_id is not None
|
|
|
|
return Response(lesson_data)
|
|
|
|
|
|
|
|
class LessonCompletionToggleAPIView(GenericAPIView):
|
|
permission_classes = [IsAuthenticated]
|
|
authentication_classes = [TokenAuthentication]
|
|
|
|
@swagger_auto_schema(
|
|
operation_description="Toggle lesson completion status (for single ID) or mark multiple lessons as completed (for list of IDs)",
|
|
tags=["Imam-Javad - Course"],
|
|
request_body=openapi.Schema(
|
|
type=openapi.TYPE_OBJECT,
|
|
required=['lesson_id'],
|
|
properties={
|
|
'lesson_id': openapi.Schema(
|
|
type=openapi.TYPE_INTEGER,
|
|
description='ID of the lesson to toggle (integer), or list of IDs to mark completed (array of integers)'
|
|
),
|
|
},
|
|
),
|
|
responses={
|
|
201: 'Lesson marked as COMPLETED.',
|
|
200: 'Lesson marked as INCOMPLETE (Unchecked) or Bulk operation completed.',
|
|
400: 'Lesson ID is required or invalid.',
|
|
404: 'Lesson not found.',
|
|
}
|
|
)
|
|
def post(self, request):
|
|
student = request.user
|
|
lesson_id_data = request.data.get('lesson_id')
|
|
|
|
if not lesson_id_data:
|
|
return Response({'error': 'Lesson ID is required.'}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
# List Case:
|
|
if isinstance(lesson_id_data, list):
|
|
try:
|
|
lesson_ids = [int(lid) for lid in lesson_id_data]
|
|
except (ValueError, TypeError):
|
|
return Response({'error': 'Invalid lesson ID in list.'}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
course_lessons = CourseLesson.objects.filter(id__in=lesson_ids)
|
|
found_ids = set(course_lessons.values_list('id', flat=True))
|
|
|
|
if any(not user_has_course_access(student, course_lesson.course) for course_lesson in course_lessons):
|
|
return Response({'error': 'You do not have access to one or more lessons.'}, status=status.HTTP_403_FORBIDDEN)
|
|
|
|
existing_completions = LessonCompletion.objects.filter(
|
|
student=student,
|
|
course_lesson_id__in=found_ids
|
|
).values_list('course_lesson_id', flat=True)
|
|
existing_completed_set = set(existing_completions)
|
|
|
|
completions_to_create = []
|
|
for cl in course_lessons:
|
|
if cl.id not in existing_completed_set:
|
|
completions_to_create.append(
|
|
LessonCompletion(student=student, course_lesson=cl)
|
|
)
|
|
|
|
if completions_to_create:
|
|
LessonCompletion.objects.bulk_create(completions_to_create)
|
|
|
|
return Response(
|
|
{
|
|
'message': f'Completed {len(completions_to_create)} new lessons.',
|
|
'completed_ids': list(found_ids)
|
|
},
|
|
status=status.HTTP_200_OK
|
|
)
|
|
|
|
# Single Integer Case:
|
|
else:
|
|
try:
|
|
lesson_id = int(lesson_id_data)
|
|
except (ValueError, TypeError):
|
|
return Response({'error': 'Invalid lesson ID.'}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
try:
|
|
course_lesson = CourseLesson.objects.get(id=lesson_id)
|
|
except CourseLesson.DoesNotExist:
|
|
return Response({'error': 'Lesson not found.'}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
if not user_has_course_access(student, course_lesson.course):
|
|
return Response({'error': 'You do not have access to this lesson.'}, status=status.HTTP_403_FORBIDDEN)
|
|
|
|
# TOGGLE LOGIC
|
|
completion = LessonCompletion.objects.filter(student=student, course_lesson=course_lesson).first()
|
|
|
|
if completion:
|
|
completion.delete()
|
|
return Response(
|
|
{'message': 'Lesson marked as incomplete.', 'is_completed': False},
|
|
status=status.HTTP_200_OK
|
|
)
|
|
else:
|
|
LessonCompletion.objects.create(student=student, course_lesson=course_lesson)
|
|
return Response(
|
|
{'message': 'Lesson completed successfully.', 'is_completed': True},
|
|
status=status.HTTP_201_CREATED
|
|
)
|
|
|
|
from django.db.models import Prefetch
|
|
class LessonListV2APIView(ListAPIView):
|
|
"""
|
|
V2 API: Returns lessons grouped by Chapters
|
|
"""
|
|
serializer_class = CourseChapterSerializer
|
|
pagination_class = StandardResultsSetPagination
|
|
permission_classes = [AllowAny]
|
|
authentication_classes = [TokenAuthentication]
|
|
|
|
@swagger_auto_schema(
|
|
operation_description=doc_courses_lesson_v2(),
|
|
tags=['Imam-Javad - Course (V2)'],
|
|
manual_parameters=[
|
|
openapi.Parameter(
|
|
'slug', openapi.IN_PATH,
|
|
description="Course slug",
|
|
type=openapi.TYPE_STRING,
|
|
required=True
|
|
),
|
|
openapi.Parameter(
|
|
'page', openapi.IN_QUERY,
|
|
description="Page number for chapter pagination",
|
|
type=openapi.TYPE_INTEGER,
|
|
required=False
|
|
),
|
|
],
|
|
responses={
|
|
200: openapi.Response(
|
|
description="Paginated list of active chapters with nested active lessons",
|
|
schema=openapi.Schema(
|
|
type=openapi.TYPE_OBJECT,
|
|
properties={
|
|
'count': openapi.Schema(type=openapi.TYPE_INTEGER),
|
|
'next': openapi.Schema(type=openapi.TYPE_STRING, format='uri', nullable=True),
|
|
'previous': openapi.Schema(type=openapi.TYPE_STRING, format='uri', nullable=True),
|
|
'results': openapi.Schema(
|
|
type=openapi.TYPE_ARRAY,
|
|
items=openapi.Schema(
|
|
type=openapi.TYPE_OBJECT,
|
|
properties={
|
|
'id': openapi.Schema(type=openapi.TYPE_INTEGER),
|
|
'title': openapi.Schema(type=openapi.TYPE_STRING),
|
|
'priority': openapi.Schema(type=openapi.TYPE_INTEGER),
|
|
'is_active': openapi.Schema(type=openapi.TYPE_BOOLEAN),
|
|
'lessons': openapi.Schema(
|
|
type=openapi.TYPE_ARRAY,
|
|
items=openapi.Schema(
|
|
type=openapi.TYPE_OBJECT,
|
|
properties={
|
|
'id': openapi.Schema(type=openapi.TYPE_INTEGER),
|
|
'title': openapi.Schema(type=openapi.TYPE_STRING),
|
|
'priority': openapi.Schema(type=openapi.TYPE_INTEGER),
|
|
'is_active': openapi.Schema(type=openapi.TYPE_BOOLEAN),
|
|
'permission': openapi.Schema(type=openapi.TYPE_BOOLEAN),
|
|
'duration': openapi.Schema(type=openapi.TYPE_INTEGER, nullable=True),
|
|
'content_type': openapi.Schema(type=openapi.TYPE_STRING, nullable=True),
|
|
'content_file': openapi.Schema(type=openapi.TYPE_STRING, nullable=True),
|
|
'video_link': openapi.Schema(type=openapi.TYPE_STRING, nullable=True),
|
|
'is_complated': openapi.Schema(type=openapi.TYPE_BOOLEAN),
|
|
'quizs': openapi.Schema(
|
|
type=openapi.TYPE_ARRAY,
|
|
nullable=True,
|
|
items=openapi.Schema(
|
|
type=openapi.TYPE_OBJECT,
|
|
properties={
|
|
'id': openapi.Schema(type=openapi.TYPE_INTEGER),
|
|
'title': openapi.Schema(type=openapi.TYPE_STRING),
|
|
'description': openapi.Schema(type=openapi.TYPE_STRING, nullable=True),
|
|
'permission': openapi.Schema(type=openapi.TYPE_BOOLEAN),
|
|
'each_question_timing': openapi.Schema(type=openapi.TYPE_INTEGER, nullable=True),
|
|
'is_complated': openapi.Schema(type=openapi.TYPE_BOOLEAN),
|
|
}
|
|
)
|
|
),
|
|
}
|
|
)
|
|
),
|
|
}
|
|
)
|
|
),
|
|
'independent_quizzes': openapi.Schema(
|
|
type=openapi.TYPE_ARRAY,
|
|
nullable=True,
|
|
items=openapi.Schema(
|
|
type=openapi.TYPE_OBJECT,
|
|
properties={
|
|
'id': openapi.Schema(type=openapi.TYPE_INTEGER),
|
|
'title': openapi.Schema(type=openapi.TYPE_STRING),
|
|
'description': openapi.Schema(type=openapi.TYPE_STRING, nullable=True),
|
|
'permission': openapi.Schema(type=openapi.TYPE_BOOLEAN),
|
|
'each_question_timing': openapi.Schema(type=openapi.TYPE_INTEGER, nullable=True),
|
|
'is_complated': openapi.Schema(type=openapi.TYPE_BOOLEAN),
|
|
}
|
|
)
|
|
),
|
|
}
|
|
)
|
|
),
|
|
404: openapi.Response(description="Course not found"),
|
|
}
|
|
)
|
|
def get(self, request, *args, **kwargs):
|
|
return super().get(request, *args, **kwargs)
|
|
|
|
def get_queryset(self):
|
|
course_slug = self.kwargs.get('slug')
|
|
course = get_object_or_404(Course, slug__contains=[{"title": course_slug}])
|
|
|
|
# We query the Chapters, and prefetch the lessons to avoid N+1 query problems
|
|
return CourseChapter.objects.prefetch_related(
|
|
Prefetch(
|
|
'lessons',
|
|
queryset=CourseLesson.objects.select_related('lesson').filter(is_active=True).order_by('priority')
|
|
)
|
|
).filter(
|
|
course=course,
|
|
is_active=True
|
|
).order_by('priority', 'id')
|
|
|
|
def list(self, request, *args, **kwargs):
|
|
queryset = self.filter_queryset(self.get_queryset())
|
|
page = self.paginate_queryset(queryset)
|
|
|
|
serializer = self.get_serializer(page if page is not None else queryset, many=True)
|
|
|
|
# Get independent quizzes belonging to this course with lesson_number = 0
|
|
course_slug = self.kwargs.get('slug')
|
|
course = get_object_or_404(Course, slug__contains=[{"title": course_slug}])
|
|
|
|
from apps.quiz.models import Quiz
|
|
from apps.quiz.serializers import QuizListSerializer
|
|
|
|
independent_quizzes_qs = Quiz.objects.filter(
|
|
course=course,
|
|
lesson_number=0,
|
|
status=True
|
|
)
|
|
independent_quizzes_data = QuizListSerializer(
|
|
independent_quizzes_qs,
|
|
many=True,
|
|
context=self.get_serializer_context()
|
|
).data
|
|
|
|
if page is not None:
|
|
response = self.get_paginated_response(serializer.data)
|
|
response.data['independent_quizzes'] = independent_quizzes_data
|
|
return response
|
|
|
|
return Response({
|
|
"independent_quizzes": independent_quizzes_data,
|
|
"results": serializer.data
|
|
})
|