Ilo/apps/accounts/api/public.py
2026-05-02 20:01:30 +03:30

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)