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 })