from __future__ import annotations from django.db import models from rest_framework import permissions, status, viewsets from rest_framework.decorators import action from rest_framework.request import Request from rest_framework.response import Response from apps.groups.models import GroupMembership, MemberProgress, ReadingGroup from apps.groups.permissions import IsGroupAdmin, IsGroupMember from apps.groups.serializers import ( AdminProgressSummarySerializer, GroupMembershipSerializer, MemberProgressPublicSerializer, MemberProgressSerializer, MemberProgressUpdateSerializer, ReadingGroupCreateSerializer, ReadingGroupDetailSerializer, ReadingGroupListSerializer, ) import logging logger = logging.getLogger(__name__) class ReadingGroupViewSet(viewsets.ModelViewSet): permission_classes = [permissions.IsAuthenticated] def get_queryset(self): return ( ReadingGroup.objects.filter(memberships__user=self.request.user) .select_related("ebook", "created_by") .prefetch_related("memberships", "memberships__progress") .distinct() ) def get_serializer_class(self): if self.action == "create": return ReadingGroupCreateSerializer if self.action == "retrieve": return ReadingGroupDetailSerializer return ReadingGroupListSerializer def perform_create(self, serializer): group = serializer.save(created_by=self.request.user) # Creator automatically becomes admin member GroupMembership.objects.create( group=group, user=self.request.user, role=GroupMembership.Role.ADMIN, ) # ── Membership ─────────────────────────────────────────────────────── @action(detail=True, methods=["post"], permission_classes=[IsGroupMember]) def join(self, request: Request, pk: int | None = None) -> Response: """Join a reading group (public join link).""" group = self.get_object() membership, created = GroupMembership.objects.get_or_create( group=group, user=request.user ) if not created: return Response({"detail": "Already a member."}, status=status.HTTP_200_OK) MemberProgress.objects.get_or_create( membership=membership, group=group, user=request.user, defaults={"current_section": 0, "percentage": 0.0}, ) return Response({"detail": "Joined successfully."}, status=status.HTTP_201_CREATED) @action(detail=True, methods=["post"], permission_classes=[IsGroupMember]) def leave(self, request: Request, pk: int | None = None) -> Response: """Leave a reading group.""" group = self.get_object() if group.created_by == request.user: return Response( {"error": "Group creator cannot leave. Transfer ownership or delete the group."}, status=status.HTTP_400_BAD_REQUEST, ) GroupMembership.objects.filter(group=group, user=request.user).delete() return Response(status=status.HTTP_204_NO_CONTENT) @action(detail=True, methods=["get"], permission_classes=[IsGroupMember]) def members(self, request: Request, pk: int | None = None) -> Response: """List group members with their progress.""" group = self.get_object() memberships = ( group.memberships.select_related("user") .prefetch_related("progress") .all() ) serializer = GroupMembershipSerializer(memberships, many=True) return Response(serializer.data) # ── Progress ───────────────────────────────────────────────────────── @action(detail=True, methods=["get"], permission_classes=[IsGroupMember], url_path="members/progress") def members_progress(self, request: Request, pk: int | None = None) -> Response: """Get progress for all group members. Other members' progress shows section label only (no last_position). Your own progress shows full detail. """ group = self.get_object() progress_qs = ( MemberProgress.objects.filter(group=group) .select_related("user") .order_by("-percentage") ) result: list[dict] = [] for mp in progress_qs: if mp.user == request.user: result.append(MemberProgressSerializer(mp).data) elif mp.is_public: result.append(MemberProgressPublicSerializer(mp).data) else: result.append({ "user_id": mp.user.id, "user_email": mp.user.email, "is_public": False, "note": "Private", }) return Response(result) @action(detail=True, methods=["get", "patch"], permission_classes=[IsGroupMember], url_path="progress") def my_progress(self, request: Request, pk: int | None = None) -> Response: """Get or update my own progress in the group.""" group = self.get_object() membership = GroupMembership.objects.get(group=group, user=request.user) progress_obj, _created = MemberProgress.objects.get_or_create( membership=membership, group=group, user=request.user, ) if request.method == "GET": serializer = MemberProgressSerializer(progress_obj) return Response(serializer.data) # PATCH — update progress update_serializer = MemberProgressUpdateSerializer(data=request.data) update_serializer.is_valid(raise_exception=True) data = update_serializer.validated_data progress_obj.update_progress( current_section=data["current_section"], percentage=data.get("percentage"), time_spent_delta=data.get("time_spent_delta", 0), last_position=data.get("last_position"), ) if "is_public" in data: progress_obj.is_public = data["is_public"] progress_obj.save(update_fields=["is_public"]) serializer = MemberProgressSerializer(progress_obj) return Response(serializer.data) # ── Admin ──────────────────────────────────────────────────────────── @action(detail=True, methods=["get"], permission_classes=[IsGroupAdmin], url_path="progress/summary") def progress_summary(self, request: Request, pk: int | None = None) -> Response: """Admin summary of all member progress.""" group = self.get_object() progress_qs = MemberProgress.objects.filter(group=group).select_related("user") total_members = group.memberships.count() members_started = progress_qs.filter(current_section__gt=0).count() members_finished = progress_qs.filter(percentage__gte=100.0).count() avg_pct = progress_qs.aggregate(avg=models.Avg("percentage"))["avg"] or 0.0 avg_time = progress_qs.aggregate(avg=models.Avg("time_spent_seconds"))["avg"] or 0.0 member_details: list[dict] = [] for mp in progress_qs: member_details.append({ "user_id": mp.user.id, "user_email": mp.user.email, "current_section": mp.current_section, "percentage": mp.percentage, "time_spent_seconds": mp.time_spent_seconds, "is_public": mp.is_public, "updated_at": mp.updated_at, }) summary_data = { "group_id": group.id, "group_name": group.name, "ebook_title": group.ebook.title, "total_members": total_members, "members_started": members_started, "members_finished": members_finished, "average_percentage": round(avg_pct, 1), "average_time_spent_hours": round(avg_time / 3600.0, 1) if avg_time else 0.0, "member_details": member_details, } serializer = AdminProgressSummarySerializer(data=summary_data) serializer.is_valid(raise_exception=True) return Response(serializer.data)