Archived
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
186 lines
6.3 KiB
Python
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
|