"""Public-facing endpoints for instructor profiles (used by the catalog UI).""" from datetime import timedelta from django.conf import settings from django.db.models import Count, IntegerField, OuterRef, Q, Subquery from django.db.models.functions import Coalesce from django.shortcuts import get_object_or_404 from django.utils import timezone from drf_spectacular.utils import OpenApiResponse, extend_schema from rest_framework import serializers from rest_framework.generics import ListAPIView from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.views import APIView from apps.accounts.models import Role, User from apps.courses.api.queries import annotate_course_stats from apps.courses.api.serializers import CourseListSerializer from apps.courses.models import Course, CourseStatus from apps.payments.models import Order, OrderStatus def _instructor_qs(): """Teachers (approved or not — all show on the catalog) annotated with stats.""" return User.objects.filter(role=Role.TEACHER).select_related("teacher_profile") def _profile(obj): """Returns the user's TeacherProfile or None (defensive — should always exist for teachers).""" return getattr(obj, "teacher_profile", None) class InstructorListItemSerializer(serializers.Serializer): id = serializers.IntegerField() full_name = serializers.CharField() headline = serializers.SerializerMethodField() is_verified = serializers.SerializerMethodField() avatar = serializers.SerializerMethodField() courses_count = serializers.IntegerField(default=0) students_count = serializers.IntegerField(default=0) average_rating = serializers.FloatField(allow_null=True, default=None) def get_headline(self, obj): tp = _profile(obj) return tp.headline if tp else "" def get_is_verified(self, obj): tp = _profile(obj) return bool(tp.is_verified) if tp else False def get_avatar(self, obj): tp = _profile(obj) if not tp or not tp.avatar: return None request = self.context.get("request") return request.build_absolute_uri(tp.avatar.url) if request else tp.avatar.url class InstructorDetailSerializer(InstructorListItemSerializer): bio = serializers.SerializerMethodField() years_experience = serializers.SerializerMethodField() def get_bio(self, obj): tp = _profile(obj) return tp.bio if tp else "" def get_years_experience(self, obj): tp = _profile(obj) return tp.years_experience if tp else None def _annotate_instructor_stats(qs): """Adds courses_count and students_count subqueries.""" courses_sq = ( Course.objects.filter(instructor=OuterRef("pk"), status=CourseStatus.PUBLISHED) .order_by() .values("instructor") .annotate(c=Count("id")) .values("c")[:1] ) students_sq = ( Order.objects.filter(course__instructor=OuterRef("pk"), status=OrderStatus.PAID) .order_by() .values("course__instructor") .annotate(c=Count("student", distinct=True)) .values("c")[:1] ) return qs.annotate( courses_count=Coalesce(Subquery(courses_sq, output_field=IntegerField()), 0), students_count=Coalesce(Subquery(students_sq, output_field=IntegerField()), 0), ) @extend_schema( tags=["instructors-public"], summary="List public instructors", description=( "Used by the catalog's 'featured instructors' strip. Each instructor includes their " "published-courses count and a unique-paid-students count. Pass `?is_featured=true` " "to scope to only the strip's curated list." ), ) class InstructorListView(ListAPIView): permission_classes = [AllowAny] pagination_class = None serializer_class = InstructorListItemSerializer ordering_fields = ["students_count", "courses_count", "id"] ordering = ["-students_count", "-courses_count"] def get_queryset(self): qs = _annotate_instructor_stats(_instructor_qs()) if self.request.query_params.get("is_featured", "").lower() in {"1", "true", "yes"}: qs = qs.filter(teacher_profile__is_featured=True) return qs @extend_schema( tags=["instructors-public"], summary="Public instructor profile", description=( "Returns the instructor's full public profile (bio, headline, avatar, verified, " "years_experience) plus stats (`courses_count`, `students_count`) and the list of their " "published courses with the standard catalog shape." ), responses={200: InstructorDetailSerializer, 404: OpenApiResponse(description="Not found.")}, ) class InstructorDetailView(APIView): permission_classes = [AllowAny] def get(self, request, instructor_id): qs = _annotate_instructor_stats(_instructor_qs()) instructor = get_object_or_404(qs, pk=instructor_id) courses = annotate_course_stats( Course.objects.filter(instructor=instructor, status=CourseStatus.PUBLISHED) .select_related("instructor", "category") ).order_by("-published_at") body = InstructorDetailSerializer(instructor, context={"request": request}).data body["courses"] = CourseListSerializer( courses, many=True, context={"request": request} ).data return Response(body)