from django.conf import settings from django.contrib.auth import get_user_model from django.db.models import Count, Q, F from django.shortcuts import get_object_or_404 from django.utils import timezone from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from rest_framework import status from rest_framework.authtoken.models import Token from rest_framework.exceptions import NotFound from rest_framework.filters import SearchFilter from rest_framework.generics import GenericAPIView, ListAPIView, RetrieveAPIView from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from apps.course.serializers import ( CourseListSerializer, CourseCategorySerializer, CourseDetailSerializer, CourseAttachmentSerializer, CourseGlossarySerializer, MyCourseListSerializer, OnlineClassTokenCreateSerializer, OnlineClassTokenVerifySerializer ) from apps.course.models import Course, CourseCategory, CourseAttachment, CourseGlossary, Participant from apps.course.doc import * from apps.account.serializers import UserProfileSerializer from utils.exceptions import AppAPIException from utils.redis import OnlineClassTokenManager UserModel = get_user_model() class CourseCategoryAPIView(ListAPIView): queryset = CourseCategory.objects.all() serializer_class = CourseCategorySerializer @swagger_auto_schema( operation_description=doc_course_category(), ) def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) class CourseListAPIView(ListAPIView): serializer_class = CourseListSerializer filter_backends = [SearchFilter] search_fields = ['title', 'category__name', 'professor__fullname'] @swagger_auto_schema( operation_description=doc_course_list(), manual_parameters=[ openapi.Parameter( 'search', openapi.IN_QUERY, description="Search by course title, category name, or professor's full name", type=openapi.TYPE_STRING, ), openapi.Parameter( 'category_slug', openapi.IN_QUERY, description="Category of the Course", type=openapi.TYPE_STRING, # enum=[category.slug for category in CourseCategory.objects.all()] ), openapi.Parameter( 'status', openapi.IN_QUERY, type=openapi.TYPE_STRING, description="""Status => Upcoming (visible but registration not allowed)---Предстоящие Registering (registration is open)---регистрация Ongoing (course has started, registration closed)---Впроцессе Finished (course has ended)---закончился """, enum=[status for status in ['upcoming', 'registering', 'ongoing', 'finished']] ), openapi.Parameter( 'is_free', openapi.IN_QUERY, description="Ценообразование is_free ", type=openapi.TYPE_BOOLEAN, ), openapi.Parameter( 'is_online', openapi.IN_QUERY, description="Статус участия is_online ", type=openapi.TYPE_BOOLEAN, ), ]) def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) def get_queryset(self): """ Optimized queryset with select_related for ForeignKey relationships and filtering """ queryset = Course.objects.select_related( 'category', 'professor' ).exclude(status=Course.StatusChoices.INACTIVE) request = self.request filters = request.query_params # Handle category_slug with multiple values separated by commas if category_slugs := filters.get('category_slug'): category_slugs_list = category_slugs.split(',') queryset = queryset.filter(category__slug__in=category_slugs_list) # Handle status with multiple values separated by commas if statuses := filters.get('status'): statuses_list = statuses.split(',') queryset = queryset.filter(status__in=statuses_list) if is_free := filters.get('is_free'): is_free = is_free.lower() == 'true' queryset = queryset.filter( Q(is_free=is_free) | Q(price=0) if is_free else Q(is_free=False, price__gt=0) ) if is_online := filters.get('is_online'): is_online = is_online.lower() == 'true' queryset = queryset.filter(is_online=is_online) return queryset class CourseDetailAPIView(RetrieveAPIView): serializer_class = CourseDetailSerializer lookup_field = "slug" def get_queryset(self): """ Optimized queryset with select_related and prefetch_related for all relationships """ return Course.objects.select_related( 'category', 'professor' ).prefetch_related( 'lessons__lesson', 'lessons__completions', 'attachments__attachment', 'glossaries__glossary', 'participants__student', 'room_messages' ) @swagger_auto_schema( operation_description=doc_course_detail(), ) def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) class MyCourseListAPIView(ListAPIView): serializer_class = MyCourseListSerializer permission_classes = [IsAuthenticated] @swagger_auto_schema(manual_parameters=[ openapi.Parameter( 'completed', openapi.IN_QUERY, description="мои курсы completed true", type=openapi.TYPE_BOOLEAN, ), openapi.Parameter( 'certificate', openapi.IN_QUERY, type=openapi.TYPE_BOOLEAN, ), ], operation_description=doc_courses_my_courses(), operation_summary="Home", ) def get(self, request, *args, **kwargs): print(f'--> my-course-> {request}/ {kwargs}') return super().get(request, *args, **kwargs) def get_queryset(self): """ Optimized queryset for user's courses with select_related and prefetch_related """ queryset = Course.objects.select_related( 'category', 'professor' ).prefetch_related( 'lessons__lesson', 'lessons__completions', 'participants__student' ).exclude(status=Course.StatusChoices.INACTIVE) request = self.request filters = request.query_params student = self.request.user qs = queryset.filter(participants__student=student) completed_only = filters.get('completed', '').lower() == 'true' if completed_only == True: # نمایش دوره‌هایی که همه درس‌هایشان توسط کاربر تکمیل شده‌اند qs = qs.annotate( total_lessons=Count('lessons', distinct=True), completed_lessons=Count( 'lessons__completions', filter=Q(lessons__completions__student=student), distinct=True ) ).filter(total_lessons=F('completed_lessons')) elif completed_only == False: # نمایش دوره‌هایی که همه درس‌هایشان تکمیل نشده‌اند qs = qs.annotate( total_lessons=Count('lessons', distinct=True), completed_lessons=Count( 'lessons__completions', filter=Q(lessons__completions__student=student), distinct=True ) ).filter(total_lessons__gt=F('completed_lessons')) if 'completed' not in filters: certificate = filters.get('certificate', '').lower() == 'true' if certificate: qs = qs.exclude( course_certificates__student=student, course_certificates__status__in=['pending', 'approved'] ) return qs class AttachmentListAPIView(ListAPIView): serializer_class = CourseAttachmentSerializer @swagger_auto_schema( manual_parameters=[ openapi.Parameter( 'slug', openapi.IN_PATH, description="Slug of the Course", type=openapi.TYPE_STRING, required=True ) ], operation_description="Retrieve a list of attachments for a given course by its slug." ) def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) def get_queryset(self): """ Optimized queryset with select_related for attachment relationship """ course_slug = self.kwargs.get('slug') try: course = Course.objects.get(slug=course_slug) except Course.DoesNotExist: raise NotFound("Course not found") return CourseAttachment.objects.select_related( 'course', 'attachment' ).filter(course=course) class GlossaryListAPIView(ListAPIView): serializer_class = CourseGlossarySerializer filter_backends = [SearchFilter] search_fields = ['glossary__title', 'glossary__description'] def get_queryset(self): """ Optimized queryset with select_related for glossary relationship """ course_slug = self.kwargs.get('slug') try: course = Course.objects.get(slug=course_slug) except Course.DoesNotExist: raise NotFound("Course not found") return CourseGlossary.objects.select_related( 'course', 'glossary' ).filter(course=course) class CourseOnlineClassTokenAPIView(GenericAPIView): permission_classes = [IsAuthenticated] serializer_class = OnlineClassTokenCreateSerializer @swagger_auto_schema( operation_description="Generate a temporary entry token for an online class.", request_body=OnlineClassTokenCreateSerializer, responses={ status.HTTP_201_CREATED: openapi.Response( description="Token generated successfully.", examples={ "application/json": { "token": "", "url": "https://frontend.example.com?token=", "expires_in": 300, } } ) } ) def post(self, request, pk, *args, **kwargs): serializer = self.get_serializer(data=request.data or {}) serializer.is_valid(raise_exception=True) course = get_object_or_404(Course, pk=pk) if not course.is_online: raise AppAPIException({'message': "Course is not marked as online."}, status_code=status.HTTP_400_BAD_REQUEST) if not self._user_has_access(request.user, course): raise AppAPIException({'message': "You do not have access to this course."}, status_code=status.HTTP_403_FORBIDDEN) manager = OnlineClassTokenManager() user_token, _ = Token.objects.get_or_create(user=request.user) identifier = f"{request.user.id}:{user_token.key[:8]}" token = manager.generate_token(course_id=course.id, user_identifier=identifier) manager.store_token(token, { 'course_id': course.id, 'user_id': request.user.id, 'user_token': user_token.key, 'extra': { 'professor_in_class': False, }, }) redirect_path = serializer.validated_data.get('redirect_path') base_url = self._build_base_url(redirect_path) entry_url = manager.build_entry_url(token, base_url=base_url) return Response({ 'token': token, 'url': entry_url, 'expires_in': getattr(settings, 'ONLINE_CLASS_TOKEN_TTL', 300), }, status=status.HTTP_201_CREATED) def _build_base_url(self, redirect_path=None) -> str: domain = getattr(settings, 'ONLINE_CLASS_FRONTEND_DOMAIN', getattr(settings, 'SITE_DOMAIN', '')).rstrip('/') if redirect_path: sanitized = redirect_path.strip('/') return f"{domain}/{sanitized}" if domain else f"/{sanitized}" return domain @staticmethod def _user_has_access(user, course: Course) -> bool: if user.is_staff or course.professor_id == user.id: return True return Participant.objects.filter(course=course, student=user).exists() class CourseOnlineClassTokenValidateAPIView(GenericAPIView): permission_classes = [AllowAny] serializer_class = OnlineClassTokenVerifySerializer @swagger_auto_schema( operation_description="Validate an online class entry token and return course/user data.", request_body=OnlineClassTokenVerifySerializer, responses={ status.HTTP_200_OK: openapi.Response( description="Token validated.", examples={ "application/json": { "course": {"id": 1, "title": "Sample Course"}, "user": {"id": 10, "fullname": "John Doe"}, "metadata": { "status": "ongoing", "has_started": True, "professor_in_class": False, "validated_at": "2024-01-01T10:00:00Z" } } } ) } ) def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) token_value = serializer.validated_data['token'] manager = OnlineClassTokenManager() payload = manager.get_payload(token_value) course_id = payload.get('course_id') user_id = payload.get('user_id') if not course_id or not user_id: raise AppAPIException({'message': 'Token payload is invalid.'}, status_code=status.HTTP_400_BAD_REQUEST) detail_view = CourseDetailAPIView() queryset = detail_view.get_queryset() course = get_object_or_404(queryset, pk=course_id) user = get_object_or_404(UserModel.objects.all(), pk=user_id) course_data = CourseDetailSerializer(course, context={'request': request}).data user_data = UserProfileSerializer(user, context={'request': request}).data metadata = self._build_metadata(course, payload) return Response({ 'course': course_data, 'user': user_data, 'metadata': metadata, }, status=status.HTTP_200_OK) def _build_metadata(self, course: Course, payload: dict) -> dict: status_value = course.status has_started = status_value in [Course.StatusChoices.ONGOING, Course.StatusChoices.FINISHED] timing_data = course.timing if isinstance(course.timing, dict) else {} return { 'status': status_value, 'is_online': course.is_online, 'has_started': has_started, 'has_finished': status_value == Course.StatusChoices.FINISHED, 'professor_in_class': payload.get('extra', {}).get('professor_in_class', False), 'scheduled_times': timing_data, 'generated_at': payload.get('generated_at'), 'validated_at': timezone.now().isoformat(), }