This repository has been archived on 2026-07-21. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
cloud-reader/backend/apps/groups/serializers.py
T
Marko 1b3b709ba5 feat: implement group EPUB upload and section splitting (US #29)
Backend:
- Create hermes Django app with models: ReadingGroup, GroupBook, Section, ReadingSchedule, MemberProgress
- EPUB section splitting service with automatic detection and reading time estimation
- Section recommendation engine for 4-week meeting schedule
- REST API endpoints for groups, books, sections, schedule, and member progress
- Manual section adjustment (merge/split) support

Frontend:
- GroupsPage: list/create reading groups
- GroupDetailPage: manage members, upload EPUB to group, view group books
- GroupBookPage: section breakdown with merge/split controls, reading schedule, member progress
- API client and TypeScript types for all group operations
- i18n keys for English and Spanish

Shared:
- Group-related types and API endpoint constants in packages/shared
2026-06-20 19:30:11 +00:00

186 lines
6.3 KiB
Python

from __future__ import annotations
from rest_framework import serializers
from apps.groups.models import (
GroupBook,
MemberProgress,
ReadingGroup,
ReadingGroupMembership,
ReadingSchedule,
Section,
)
from apps.books.serializers import EBookListSerializer
class ReadingGroupMembershipSerializer(serializers.ModelSerializer):
user_email = serializers.CharField(source="user.email", read_only=True)
user_username = serializers.CharField(source="user.username", read_only=True)
class Meta:
model = ReadingGroupMembership
fields = [
"id", "user", "user_email", "user_username",
"role", "joined_at",
]
read_only_fields = ["id", "joined_at"]
class ReadingGroupListSerializer(serializers.ModelSerializer):
member_count = serializers.SerializerMethodField()
admin_email = serializers.CharField(source="admin.email", read_only=True)
class Meta:
model = ReadingGroup
fields = [
"id", "name", "description", "admin", "admin_email",
"member_count", "created_at", "updated_at",
]
def get_member_count(self, obj: ReadingGroup) -> int:
return obj.memberships.count()
class ReadingGroupDetailSerializer(serializers.ModelSerializer):
admin_email = serializers.CharField(source="admin.email", read_only=True)
members = ReadingGroupMembershipSerializer(source="memberships", many=True, read_only=True)
class Meta:
model = ReadingGroup
fields = [
"id", "name", "description", "admin", "admin_email",
"members", "created_at", "updated_at",
]
class ReadingGroupCreateSerializer(serializers.ModelSerializer):
class Meta:
model = ReadingGroup
fields = ["name", "description"]
class AddMemberSerializer(serializers.Serializer):
user_id = serializers.IntegerField()
def validate_user_id(self, value: int) -> int:
from django.conf import settings
User = settings.AUTH_USER_MODEL
if not User.objects.filter(id=value).exists():
raise serializers.ValidationError("User not found.")
return value
class SectionSerializer(serializers.ModelSerializer):
class Meta:
model = Section
fields = [
"id", "title", "order", "start_chapter_index",
"end_chapter_index", "estimated_reading_minutes", "created_at",
]
read_only_fields = ["id", "created_at"]
class SectionAdjustSerializer(serializers.Serializer):
"""Payload for manual section adjustments (merge/split)."""
operation = serializers.ChoiceField(choices=["merge", "split"])
section_ids = serializers.ListField(
child=serializers.IntegerField(), min_length=1,
help_text="For merge: list of section IDs to merge. For split: [section_id] to split."
)
split_at = serializers.IntegerField(
required=False, default=2, min_value=2,
help_text="Number of new sections when splitting"
)
class ReadingScheduleSerializer(serializers.ModelSerializer):
section_details = serializers.SerializerMethodField()
class Meta:
model = ReadingSchedule
fields = [
"id", "meeting_number", "week_date", "section_ids",
"section_details", "created_at",
]
read_only_fields = ["id", "created_at"]
def get_section_details(self, obj: ReadingSchedule) -> list[dict]:
sections = Section.objects.filter(
id__in=obj.section_ids, group_book=obj.group_book
).order_by("order")
return SectionSerializer(sections, many=True).data
class MemberProgressSerializer(serializers.ModelSerializer):
user_email = serializers.CharField(source="user.email", read_only=True)
user_username = serializers.CharField(source="user.username", read_only=True)
current_section_title = serializers.SerializerMethodField()
class Meta:
model = MemberProgress
fields = [
"id", "user", "user_email", "user_username",
"current_section", "current_section_title",
"completed_sections", "updated_at",
]
read_only_fields = ["id", "updated_at"]
def get_current_section_title(self, obj: MemberProgress) -> str | None:
if obj.current_section:
return obj.current_section.title
return None
class GroupBookListSerializer(serializers.ModelSerializer):
ebook = EBookListSerializer(read_only=True)
uploaded_by_email = serializers.CharField(source="uploaded_by.email", read_only=True)
section_count = serializers.SerializerMethodField()
class Meta:
model = GroupBook
fields = [
"id", "title", "status", "ebook", "uploaded_by",
"uploaded_by_email", "section_count", "created_at", "updated_at",
]
def get_section_count(self, obj: GroupBook) -> int:
return obj.sections.count()
class GroupBookDetailSerializer(serializers.ModelSerializer):
ebook = EBookListSerializer(read_only=True)
uploaded_by_email = serializers.CharField(source="uploaded_by.email", read_only=True)
sections = SectionSerializer(many=True, read_only=True)
schedules = ReadingScheduleSerializer(source="schedules", many=True, read_only=True)
class Meta:
model = GroupBook
fields = [
"id", "group", "title", "status", "ebook",
"uploaded_by", "uploaded_by_email",
"sections", "schedules", "created_at", "updated_at",
]
class GroupBookCreateSerializer(serializers.Serializer):
ebook_id = serializers.IntegerField()
title = serializers.CharField(max_length=512, required=False)
def validate_ebook_id(self, value: int) -> int:
from apps.books.models import EBook
if not EBook.objects.filter(id=value).exists():
raise serializers.ValidationError("EBook not found.")
# Check format is EPUB
ebook = EBook.objects.get(id=value)
if ebook.format != "epub":
raise serializers.ValidationError("Only EPUB format is supported for group books.")
return value
def validate(self, data: dict) -> dict:
request = self.context.get("request")
if request and not data.get("title"):
from apps.books.models import EBook
ebook = EBook.objects.get(id=data["ebook_id"])
data["title"] = ebook.title
return data