112 lines
4.5 KiB
Python
112 lines
4.5 KiB
Python
"""Unified `/api/search/` across courses + instructors (public).
|
|
|
|
If the requester is an authenticated student, also returns matching lessons in
|
|
courses they're enrolled in. Designed for the topbar's command palette / search box.
|
|
"""
|
|
|
|
from django.db.models import Q
|
|
from drf_spectacular.utils import OpenApiParameter, extend_schema, inline_serializer
|
|
from rest_framework import serializers
|
|
from rest_framework.permissions import AllowAny
|
|
from rest_framework.response import Response
|
|
from rest_framework.views import APIView
|
|
|
|
from apps.accounts.api.public import (
|
|
InstructorListItemSerializer,
|
|
_annotate_instructor_stats,
|
|
_instructor_qs,
|
|
)
|
|
from apps.accounts.models import Role
|
|
from apps.courses.api.queries import annotate_course_stats
|
|
from apps.courses.api.serializers import CourseListSerializer
|
|
from apps.courses.models import Course, CourseStatus, Lesson, LessonStatus
|
|
from apps.enrollments.models import Enrollment
|
|
|
|
|
|
class _LessonResultSerializer(serializers.ModelSerializer):
|
|
course_id = serializers.IntegerField(source="section.course.id", read_only=True)
|
|
course_slug = serializers.CharField(source="section.course.slug", read_only=True)
|
|
course_title = serializers.CharField(source="section.course.title", read_only=True)
|
|
|
|
class Meta:
|
|
model = Lesson
|
|
fields = ["id", "title", "kind", "course_id", "course_slug", "course_title"]
|
|
|
|
|
|
class GlobalSearchView(APIView):
|
|
"""`GET /api/search/?q=...&types=courses,instructors,lessons&limit=5`."""
|
|
|
|
permission_classes = [AllowAny]
|
|
|
|
@extend_schema(
|
|
tags=["search"],
|
|
summary="Global search across courses + instructors (+ lessons for enrolled students)",
|
|
description=(
|
|
"Returns mixed results grouped by entity. `?types=` is a comma-separated whitelist; "
|
|
"default is `courses,instructors`. Lessons are only searched for authenticated students "
|
|
"and only within courses they're enrolled in. `?limit=` caps each group (default 5, max 20)."
|
|
),
|
|
parameters=[
|
|
OpenApiParameter(name="q", required=True, type=str, location=OpenApiParameter.QUERY),
|
|
OpenApiParameter(name="types", required=False, type=str, location=OpenApiParameter.QUERY),
|
|
OpenApiParameter(name="limit", required=False, type=int, location=OpenApiParameter.QUERY),
|
|
],
|
|
)
|
|
def get(self, request):
|
|
q = (request.query_params.get("q") or "").strip()
|
|
if not q:
|
|
return Response({"detail": "missing `q`"}, status=400)
|
|
try:
|
|
limit = max(1, min(int(request.query_params.get("limit") or 5), 20))
|
|
except ValueError:
|
|
limit = 5
|
|
types_raw = request.query_params.get("types") or "courses,instructors"
|
|
wanted = {t.strip() for t in types_raw.split(",") if t.strip()}
|
|
|
|
out: dict = {"q": q}
|
|
|
|
if "courses" in wanted:
|
|
cqs = (
|
|
Course.objects.filter(status=CourseStatus.PUBLISHED)
|
|
.filter(
|
|
Q(title__icontains=q)
|
|
| Q(title_en__icontains=q)
|
|
| Q(title_fa__icontains=q)
|
|
| Q(tagline__icontains=q)
|
|
)
|
|
.select_related("instructor", "category")
|
|
)
|
|
cqs = annotate_course_stats(cqs)[:limit]
|
|
out["courses"] = CourseListSerializer(cqs, many=True, context={"request": request}).data
|
|
|
|
if "instructors" in wanted:
|
|
iqs = (
|
|
_annotate_instructor_stats(_instructor_qs())
|
|
.filter(full_name__icontains=q)
|
|
)[:limit]
|
|
out["instructors"] = InstructorListItemSerializer(
|
|
iqs, many=True, context={"request": request}
|
|
).data
|
|
|
|
if (
|
|
"lessons" in wanted
|
|
and request.user.is_authenticated
|
|
and getattr(request.user, "role", None) == Role.STUDENT
|
|
):
|
|
enrolled_course_ids = list(
|
|
Enrollment.objects.filter(student=request.user).values_list("course_id", flat=True)
|
|
)
|
|
lqs = (
|
|
Lesson.objects.filter(
|
|
section__course_id__in=enrolled_course_ids,
|
|
status=LessonStatus.PUBLISHED,
|
|
)
|
|
.filter(
|
|
Q(title__icontains=q) | Q(title_en__icontains=q) | Q(title_fa__icontains=q)
|
|
)
|
|
.select_related("section__course")
|
|
)[:limit]
|
|
out["lessons"] = _LessonResultSerializer(lqs, many=True).data
|
|
|
|
return Response(out)
|