import logging from django.db.models import Q from django.shortcuts import get_object_or_404 from django.utils import timezone from django.utils.translation import gettext_lazy as _ from rest_framework import status from rest_framework.views import APIView from rest_framework.generics import GenericAPIView from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from drf_spectacular.utils import extend_schema, OpenApiResponse, OpenApiParameter from apps.lms.models import ( Course, CourseModule, Lesson, QuizQuestion, CourseEnrollment, LessonProgress, DigitalCertificate, ) from apps.lms.permissions import IsCourseInstructorOrAdmin from apps.lms.serializers import ( CourseListSerializer, CourseDetailSerializer, CourseCreateUpdateSerializer, CourseModuleSerializer, LessonDetailSerializer, QuizSubmitRequestSerializer, QuizResultSerializer, CourseProgressSerializer, LessonCompleteResponseSerializer, DigitalCertificateSerializer, PublicCertificateVerifySerializer, ) logger = logging.getLogger(__name__) def get_course_by_pk_or_slug(pk_or_slug, queryset=None): if queryset is None: queryset = Course.objects.all() if pk_or_slug.isdigit(): return get_object_or_404(queryset, pk=int(pk_or_slug)) return get_object_or_404(queryset, slug=pk_or_slug) class CourseListCreateView(GenericAPIView): permission_classes = [AllowAny] serializer_class = CourseListSerializer queryset = Course.objects.filter(is_published=True) @extend_schema( summary="List online academy courses", description="Retrieves a list of published courses with category, language, level, and keyword filtering.", parameters=[ OpenApiParameter('category', str, description="Filter by course category (e.g. razavi_teachings, cultural_diplomacy)"), OpenApiParameter('language', str, description="Filter by language code (fa, ar, en, ur, fr)"), OpenApiParameter('level', str, description="Filter by difficulty level (introductory, intermediate, advanced)"), OpenApiParameter('search', str, description="Keyword search in title, instructor, or description"), OpenApiParameter('featured', bool, description="Filter only featured courses"), ], responses={200: CourseListSerializer(many=True)}, tags=["LMS - Online Academy"], ) def get(self, request, *args, **kwargs): qs = Course.objects.all() # If user is not staff, only show published courses if not (request.user and request.user.is_authenticated and request.user.is_staff): qs = qs.filter(is_published=True) category = request.query_params.get('category') if category: qs = qs.filter(category=category) language = request.query_params.get('language') if language: qs = qs.filter(language=language) level = request.query_params.get('level') if level: qs = qs.filter(level=level) featured = request.query_params.get('featured') if featured is not None: qs = qs.filter(is_featured=featured.lower() in ('true', '1')) search = request.query_params.get('search') if search: qs = qs.filter( Q(title__icontains=search) | Q(description__icontains=search) | Q(instructor_name__icontains=search) | Q(organization_name__icontains=search) ) serializer = self.get_serializer(qs, many=True, context={'request': request}) return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema( summary="Create a new course", description="Creates a new LMS course. Creator is automatically set as course author.", request=CourseCreateUpdateSerializer, responses={ 201: CourseDetailSerializer, 400: OpenApiResponse(description="Validation error"), 403: OpenApiResponse(description="Permission denied"), }, tags=["LMS - Online Academy"], ) def post(self, request, *args, **kwargs): if not request.user or not request.user.is_authenticated: return Response({'detail': _("Authentication credentials were not provided.")}, status=status.HTTP_401_UNAUTHORIZED) serializer = CourseCreateUpdateSerializer(data=request.data) serializer.is_valid(raise_exception=True) course = serializer.save(created_by=request.user) response_serializer = CourseDetailSerializer(course, context={'request': request}) return Response(response_serializer.data, status=status.HTTP_201_CREATED) class CourseDetailView(GenericAPIView): permission_classes = [AllowAny] serializer_class = CourseDetailSerializer queryset = Course.objects.all() @extend_schema( summary="Retrieve course syllabus and details", description="Returns full course information, curriculum modules, lessons list, instructor details, and authenticated user enrollment status.", responses={ 200: CourseDetailSerializer, 404: OpenApiResponse(description="Course not found"), }, tags=["LMS - Online Academy"], ) def get(self, request, pk_or_slug, *args, **kwargs): course = get_course_by_pk_or_slug(pk_or_slug, self.get_queryset()) serializer = self.get_serializer(course, context={'request': request}) return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema( summary="Update course metadata", description="Partially updates course information. Restricted to instructor or platform administrator.", request=CourseCreateUpdateSerializer, responses={ 200: CourseDetailSerializer, 403: OpenApiResponse(description="Permission denied"), }, tags=["LMS - Online Academy"], ) def patch(self, request, pk_or_slug, *args, **kwargs): course = get_course_by_pk_or_slug(pk_or_slug, self.get_queryset()) self.check_object_permissions(request, course) serializer = CourseCreateUpdateSerializer(course, data=request.data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() response_serializer = CourseDetailSerializer(course, context={'request': request}) return Response(response_serializer.data, status=status.HTTP_200_OK) @extend_schema( summary="Delete a course", description="Deletes a course and its modules. Restricted to instructor or platform administrator.", responses={204: OpenApiResponse(description="Course deleted")}, tags=["LMS - Online Academy"], ) def delete(self, request, pk_or_slug, *args, **kwargs): course = get_course_by_pk_or_slug(pk_or_slug, self.get_queryset()) self.check_object_permissions(request, course) course.delete() return Response(status=status.HTTP_204_NO_CONTENT) class CourseEnrollView(GenericAPIView): permission_classes = [IsAuthenticated] serializer_class = CourseDetailSerializer queryset = Course.objects.all() @extend_schema( summary="Enroll current user in course", description="Enrolls the authenticated user into the specified course. Returns enrollment status.", responses={ 200: OpenApiResponse(description="Already enrolled"), 201: OpenApiResponse(description="Successfully enrolled"), 404: OpenApiResponse(description="Course not found"), }, tags=["LMS - Online Academy"], ) def post(self, request, pk_or_slug, *args, **kwargs): course = get_course_by_pk_or_slug(pk_or_slug, self.get_queryset()) enrollment, created = CourseEnrollment.objects.get_or_create( user=request.user, course=course ) status_code = status.HTTP_201_CREATED if created else status.HTTP_200_OK return Response({ 'message': _("Enrolled successfully.") if created else _("Already enrolled in this course."), 'course_id': course.id, 'course_title': course.title, 'enrolled_at': enrollment.enrolled_at, 'progress_percentage': enrollment.progress_percentage, }, status=status_code) class CourseProgressView(GenericAPIView): permission_classes = [IsAuthenticated] serializer_class = CourseProgressSerializer queryset = Course.objects.all() @extend_schema( summary="Get user progress in course", description="Returns completion percentage, completed lessons tally, and certificate code if finished.", responses={ 200: CourseProgressSerializer, 404: OpenApiResponse(description="Course or enrollment not found"), }, tags=["LMS - Online Academy"], ) def get(self, request, pk_or_slug, *args, **kwargs): course = get_course_by_pk_or_slug(pk_or_slug, self.get_queryset()) enrollment = CourseEnrollment.objects.filter(user=request.user, course=course).first() all_lesson_ids = Lesson.objects.filter(module__course=course).values_list('id', flat=True) total_lessons = len(all_lesson_ids) completed_lessons = LessonProgress.objects.filter( user=request.user, lesson_id__in=all_lesson_ids, is_completed=True ).count() cert = DigitalCertificate.objects.filter(user=request.user, course=course).first() data = { 'course_id': course.id, 'course_title': course.title, 'progress_percentage': enrollment.progress_percentage if enrollment else 0, 'completed_lessons_count': completed_lessons, 'total_lessons_count': total_lessons, 'is_completed': (enrollment.progress_percentage >= 100) if enrollment else False, 'certificate_code': cert.certificate_code if cert else None, } return Response(data, status=status.HTTP_200_OK) class LessonDetailView(GenericAPIView): permission_classes = [AllowAny] serializer_class = LessonDetailSerializer queryset = Lesson.objects.all() @extend_schema( summary="Get lesson details and content", description="Retrieves lesson video link, markdown content, slides attachment, and quiz questions.", responses={ 200: LessonDetailSerializer, 404: OpenApiResponse(description="Lesson not found"), }, tags=["LMS - Online Academy"], ) def get(self, request, pk, *args, **kwargs): lesson = get_object_or_404(Lesson, pk=pk) serializer = self.get_serializer(lesson, context={'request': request}) return Response(serializer.data, status=status.HTTP_200_OK) class LessonCompleteView(GenericAPIView): permission_classes = [IsAuthenticated] serializer_class = LessonCompleteResponseSerializer queryset = Lesson.objects.all() @extend_schema( summary="Mark lesson as complete", description="Marks a reading or video lesson as complete. Automatically recalculates course progress and issues certificate if 100%.", responses={ 200: LessonCompleteResponseSerializer, 404: OpenApiResponse(description="Lesson not found"), }, tags=["LMS - Online Academy"], ) def post(self, request, pk, *args, **kwargs): lesson = get_object_or_404(Lesson, pk=pk) course = lesson.module.course # Ensure enrollment exists enrollment, _created = CourseEnrollment.objects.get_or_create( user=request.user, course=course ) progress, _p_created = LessonProgress.objects.get_or_create( user=request.user, lesson=lesson ) progress.is_completed = True progress.completed_at = timezone.now() progress.save() # Recalculate course overall progress new_progress = enrollment.recalculate_progress() cert = DigitalCertificate.objects.filter(user=request.user, course=course).first() return Response({ 'message': _("Lesson marked as complete."), 'lesson_id': lesson.id, 'is_completed': True, 'course_progress_percentage': new_progress, 'certificate_code': cert.certificate_code if cert else None, }, status=status.HTTP_200_OK) class LessonQuizSubmitView(GenericAPIView): permission_classes = [IsAuthenticated] serializer_class = QuizSubmitRequestSerializer queryset = Lesson.objects.all() @extend_schema( summary="Submit quiz answers and evaluate score", description="Evaluates user quiz responses. If score >= 70%, marks lesson complete, recalculates progress, and issues certificate if 100%.", request=QuizSubmitRequestSerializer, responses={ 200: QuizResultSerializer, 400: OpenApiResponse(description="Invalid submission"), 404: OpenApiResponse(description="Lesson not found"), }, tags=["LMS - Online Academy"], ) def post(self, request, pk, *args, **kwargs): lesson = get_object_or_404(Lesson, pk=pk) course = lesson.module.course serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) answers = serializer.validated_data['answers'] # Ensure user is enrolled enrollment, _e_created = CourseEnrollment.objects.get_or_create( user=request.user, course=course ) questions = lesson.quiz_questions.all().order_by('order', 'id') total_questions = questions.count() if total_questions == 0: return Response({'error': _("No questions found in this quiz.")}, status=status.HTTP_400_BAD_REQUEST) # Map answers answers_map = {ans['question_id']: ans['selected_option'] for ans in answers} correct_count = 0 detailed_results = [] for q in questions: user_choice = answers_map.get(q.id) is_correct = (user_choice == q.correct_option_index) if is_correct: correct_count += 1 detailed_results.append({ 'question_id': q.id, 'is_correct': is_correct, 'selected_option': user_choice if user_choice is not None else -1, 'correct_option': q.correct_option_index, 'explanation': q.explanation or '', }) score = round((correct_count / total_questions) * 100.0, 1) passed = score >= 70.0 progress, _prog_created = LessonProgress.objects.get_or_create( user=request.user, lesson=lesson ) progress.quiz_score = score if passed: progress.is_completed = True progress.completed_at = timezone.now() progress.save() # Recalculate progress if passed new_progress = enrollment.recalculate_progress() cert = DigitalCertificate.objects.filter(user=request.user, course=course).first() result_data = { 'score': score, 'passed': passed, 'total_questions': total_questions, 'correct_count': correct_count, 'question_results': detailed_results, 'is_lesson_completed': progress.is_completed, 'course_progress_percentage': new_progress, 'certificate_code': cert.certificate_code if cert else None, } return Response(result_data, status=status.HTTP_200_OK) class DigitalCertificateListView(GenericAPIView): permission_classes = [IsAuthenticated] serializer_class = DigitalCertificateSerializer queryset = DigitalCertificate.objects.all() def get_queryset(self): if getattr(self, 'swagger_fake_view', False) or not self.request.user.is_authenticated: return DigitalCertificate.objects.none() return DigitalCertificate.objects.filter(user=self.request.user, is_revoked=False) @extend_schema( summary="List user earned digital certificates", description="Retrieves a list of all official digital certificates earned by the authenticated user across completed academy courses.", responses={200: DigitalCertificateSerializer(many=True)}, tags=["LMS - Online Academy"], ) def get(self, request, *args, **kwargs): queryset = self.get_queryset() serializer = self.get_serializer(queryset, many=True) return Response(serializer.data, status=status.HTTP_200_OK) class PublicCertificateVerifyView(GenericAPIView): permission_classes = [AllowAny] serializer_class = PublicCertificateVerifySerializer queryset = DigitalCertificate.objects.all() @extend_schema( summary="Verify digital certificate authenticity (Public / QR)", description="Public endpoint for verifying credentials by certificate code or UUID. Does not require login.", responses={ 200: PublicCertificateVerifySerializer, 404: OpenApiResponse(description="Certificate code not found"), }, tags=["LMS - Online Academy"], ) def get(self, request, code, *args, **kwargs): # Look up by code or uuid cert = None try: cert = DigitalCertificate.objects.filter( Q(certificate_code__iexact=code.strip()) | Q(uuid__iexact=code.strip()) ).first() except Exception: pass if not cert: return Response({ 'is_valid': False, 'detail': _("Certificate with this verification code was not found or has been revoked.") }, status=status.HTTP_404_NOT_FOUND) data = { 'certificate_code': cert.certificate_code, 'uuid': cert.uuid, 'recipient_name': cert.user.get_full_name(), 'course_title': cert.course.title, 'organization': cert.course.organization_name or (_("Razavi International Hub") if not cert.course.institution else cert.course.institution.name), 'issue_date': cert.issue_date, 'final_score': cert.final_score, 'is_valid': not cert.is_revoked, } return Response(data, status=status.HTTP_200_OK)