from rest_framework import serializers from .models import UserCheckpointProgress, UserProgress class UserProgressSerializer(serializers.ModelSerializer): resource_title = serializers.CharField(source='resource.title', read_only=True) has_checkpoints = serializers.SerializerMethodField() in_current_plan = serializers.SerializerMethodField() class Meta: model = UserProgress fields = ['id', 'resource', 'resource_title', 'status', 'progress', 'started_at', 'completed_at', 'has_checkpoints', 'in_current_plan'] read_only_fields = ['status', 'started_at', 'completed_at', 'resource_title', 'has_checkpoints', 'in_current_plan'] def get_has_checkpoints(self, obj) -> bool: # Prefer the annotated value from the list view's queryset; fall back # to a direct count on single-object endpoints. annotated = getattr(obj, 'resource_has_checkpoints', None) if annotated is not None: return annotated # ResourceProgressView (detail) sets no prefetch_related, so this is # one extra query on a single object — not an N+1, fine off the list. return len(obj.resource.checkpoints.all()) > 0 def get_in_current_plan(self, obj) -> bool: # Annotated by UserProgressListView.get_queryset. Single-object # endpoints (ResourceProgressView) don't annotate it; default False — # the frontend only consumes this flag on the list to disable # out-of-plan rows, so a conservative False off the detail path is safe. annotated = getattr(obj, 'in_current_plan_annot', None) return bool(annotated) if annotated is not None else False class UserCheckpointProgressSerializer(serializers.ModelSerializer): class Meta: model = UserCheckpointProgress fields = ['id', 'checkpoint', 'completed_at'] read_only_fields = ['completed_at']