from datetime import timedelta from django.conf import settings from django.utils import timezone from rest_framework import serializers from apps.courses.models import ( Category, Course, CourseFAQ, CourseHighlight, Lesson, Section, ) # --- Categories --------------------------------------------------------------- class CategorySerializer(serializers.ModelSerializer): course_count = serializers.IntegerField(read_only=True, default=0) class Meta: model = Category fields = ["id", "slug", "name", "name_fa", "name_en", "order", "course_count"] read_only_fields = ["id", "course_count"] # --- Course bits -------------------------------------------------------------- class CourseHighlightSerializer(serializers.ModelSerializer): """Read-only nested representation.""" class Meta: model = CourseHighlight fields = ["id", "text", "order"] read_only_fields = ["id"] class CourseHighlightWriteSerializer(serializers.ModelSerializer): course = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all()) class Meta: model = CourseHighlight fields = ["id", "course", "text", "order"] read_only_fields = ["id"] class CourseFAQSerializer(serializers.ModelSerializer): """Read-only nested representation.""" class Meta: model = CourseFAQ fields = ["id", "question", "answer", "order"] read_only_fields = ["id"] class CourseFAQWriteSerializer(serializers.ModelSerializer): course = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all()) class Meta: model = CourseFAQ fields = ["id", "course", "question", "answer", "order"] read_only_fields = ["id"] # --- Lessons ------------------------------------------------------------------ class LessonWriteSerializer(serializers.ModelSerializer): """Used for create/update by the instructor.""" section = serializers.PrimaryKeyRelatedField(queryset=Section.objects.all()) class Meta: model = Lesson fields = [ "id", "section", "title", "kind", "body", "video_url", "pdf_file", "duration_seconds", "is_preview", "is_downloadable", "status", "order", ] read_only_fields = ["id"] def validate(self, attrs): kind = attrs.get("kind") or getattr(self.instance, "kind", None) body = attrs.get("body") or (getattr(self.instance, "body", "") if self.instance else "") video_url = attrs.get("video_url") or ( getattr(self.instance, "video_url", "") if self.instance else "" ) pdf_file = attrs.get("pdf_file") or ( getattr(self.instance, "pdf_file", None) if self.instance else None ) if kind == "video" and not video_url: raise serializers.ValidationError({"video_url": "Required when kind=video."}) if kind == "pdf" and not pdf_file: raise serializers.ValidationError({"pdf_file": "Required when kind=pdf."}) if kind in {"text", "task"} and not body: raise serializers.ValidationError({"body": "Required when kind=text or kind=task."}) return attrs class LessonReadSerializer(serializers.ModelSerializer): """Used for nested reads. Hides body/video_url/pdf for non-preview lessons in public detail (the public detail serializer overrides those with a PreviewLessonSerializer below).""" class Meta: model = Lesson fields = [ "id", "title", "kind", "body", "video_url", "pdf_file", "duration_seconds", "is_preview", "is_downloadable", "status", "order", ] class PublicLessonSerializer(serializers.ModelSerializer): """For public catalog: only expose content for free previews; otherwise hide it.""" body = serializers.SerializerMethodField() video_url = serializers.SerializerMethodField() pdf_file = serializers.SerializerMethodField() class Meta: model = Lesson fields = [ "id", "title", "kind", "body", "video_url", "pdf_file", "duration_seconds", "is_preview", "is_downloadable", "order", ] def _gated(self, obj, attr): return getattr(obj, attr) if obj.is_preview else None def get_body(self, obj): return self._gated(obj, "body") def get_video_url(self, obj): return self._gated(obj, "video_url") def get_pdf_file(self, obj): f = self._gated(obj, "pdf_file") request = self.context.get("request") if f and request: return request.build_absolute_uri(f.url) return f.url if f else None # --- Sections ----------------------------------------------------------------- class SectionWriteSerializer(serializers.ModelSerializer): course = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all()) class Meta: model = Section fields = ["id", "course", "title", "order"] read_only_fields = ["id"] class SectionReadSerializer(serializers.ModelSerializer): lessons = LessonReadSerializer(many=True, read_only=True) class Meta: model = Section fields = ["id", "title", "order", "lessons"] class PublicSectionSerializer(serializers.ModelSerializer): lessons = serializers.SerializerMethodField() class Meta: model = Section fields = ["id", "title", "order", "lessons"] def get_lessons(self, obj): published = obj.lessons.filter(status="published") return PublicLessonSerializer(published, many=True, context=self.context).data # --- Courses ------------------------------------------------------------------ class _CourseStatsMixin(serializers.Serializer): """Adds annotated stats + computed badges to any Course serializer. Requires the queryset to be passed through `annotate_course_stats()` first; otherwise the values default to 0 / False.""" lesson_count = serializers.IntegerField(read_only=True, default=0) enrolled_count = serializers.IntegerField(read_only=True, default=0) total_minutes_seconds = serializers.IntegerField(read_only=True, default=0) is_new = serializers.SerializerMethodField() is_bestseller = serializers.SerializerMethodField() discount_active = serializers.SerializerMethodField() def get_is_new(self, obj): window = getattr(settings, "COURSE_NEW_WINDOW_DAYS", 30) return (timezone.now() - obj.created_at) < timedelta(days=window) def get_is_bestseller(self, obj): threshold = getattr(settings, "COURSE_BESTSELLER_THRESHOLD", 10) return getattr(obj, "popularity", 0) >= threshold def get_discount_active(self, obj): return obj.discount_is_active class CourseListSerializer(_CourseStatsMixin, serializers.ModelSerializer): """Shape used in /api/instructor/courses/ list and /api/courses/ list.""" instructor_id = serializers.IntegerField(read_only=True) instructor_name = serializers.CharField(source="instructor.full_name", read_only=True) category = CategorySerializer(read_only=True) cover_image = serializers.SerializerMethodField() class Meta: model = Course fields = [ "id", "slug", "title", "tagline", "level", "language", "color", "cover_image", "status", "is_coming_soon", "category", "instructor_id", "instructor_name", "price_toman", "original_price_toman", "created_at", "updated_at", "published_at", "lesson_count", "enrolled_count", "total_minutes_seconds", "is_new", "is_bestseller", "discount_active", "discount_starts_at", "discount_ends_at", ] read_only_fields = fields def get_cover_image(self, obj): if not obj.cover_image: return None request = self.context.get("request") if request: return request.build_absolute_uri(obj.cover_image.url) return obj.cover_image.url class CourseInstructorWriteSerializer(serializers.ModelSerializer): """Used by the instructor for create / update. Slug is auto-generated if omitted.""" class Meta: model = Course fields = [ "id", "slug", "title", "tagline", "description", "level", "language", "category", "cover_image", "color", "is_coming_soon", "price_toman", "original_price_toman", ] read_only_fields = ["id", "slug"] class CourseInstructorDetailSerializer(_CourseStatsMixin, serializers.ModelSerializer): """Full instructor-side view: includes drafts, sections, lessons, highlights, FAQ.""" category = CategorySerializer(read_only=True) sections = SectionReadSerializer(many=True, read_only=True) highlights = CourseHighlightSerializer(many=True, read_only=True) faqs = CourseFAQSerializer(many=True, read_only=True) cover_image = serializers.SerializerMethodField() class Meta: model = Course fields = [ "id", "slug", "title", "tagline", "description", "level", "language", "color", "cover_image", "status", "is_coming_soon", "published_at", "created_at", "updated_at", "category", "price_toman", "original_price_toman", "sections", "highlights", "faqs", "lesson_count", "enrolled_count", "total_minutes_seconds", "is_new", "is_bestseller", "discount_active", "discount_starts_at", "discount_ends_at", ] read_only_fields = fields def get_cover_image(self, obj): if not obj.cover_image: return None request = self.context.get("request") if request: return request.build_absolute_uri(obj.cover_image.url) return obj.cover_image.url class CoursePublicDetailSerializer(_CourseStatsMixin, serializers.ModelSerializer): """Public course detail: only published sections + published lessons; preview-gated content.""" instructor_id = serializers.IntegerField(read_only=True) instructor_name = serializers.CharField(source="instructor.full_name", read_only=True) instructor = serializers.SerializerMethodField() category = CategorySerializer(read_only=True) sections = serializers.SerializerMethodField() highlights = CourseHighlightSerializer(many=True, read_only=True) faqs = CourseFAQSerializer(many=True, read_only=True) cover_image = serializers.SerializerMethodField() class Meta: model = Course fields = [ "id", "slug", "title", "tagline", "description", "level", "language", "color", "cover_image", "published_at", "created_at", "updated_at", "category", "instructor_id", "instructor_name", "instructor", "price_toman", "original_price_toman", "highlights", "faqs", "sections", "lesson_count", "enrolled_count", "total_minutes_seconds", "is_new", "is_bestseller", "discount_active", "discount_starts_at", "discount_ends_at", "is_coming_soon", ] read_only_fields = fields def get_instructor(self, obj): # Embedded "About instructor" card. Falls back gracefully if no profile yet. u = obj.instructor tp = getattr(u, "teacher_profile", None) request = self.context.get("request") avatar_url = None if tp and tp.avatar: avatar_url = request.build_absolute_uri(tp.avatar.url) if request else tp.avatar.url return { "id": u.id, "full_name": u.full_name, "headline": getattr(tp, "headline", "") if tp else "", "bio": getattr(tp, "bio", "") if tp else "", "is_verified": getattr(tp, "is_verified", False) if tp else False, "years_experience": getattr(tp, "years_experience", None) if tp else None, "avatar": avatar_url, } def get_cover_image(self, obj): if not obj.cover_image: return None request = self.context.get("request") if request: return request.build_absolute_uri(obj.cover_image.url) return obj.cover_image.url def get_sections(self, obj): published = obj.sections.all().prefetch_related("lessons") return PublicSectionSerializer(published, many=True, context=self.context).data # --- Reorder ------------------------------------------------------------------ class IDListSerializer(serializers.Serializer): ids = serializers.ListField(child=serializers.IntegerField(), allow_empty=False)