145 lines
5.3 KiB
Python
145 lines
5.3 KiB
Python
"""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)
|