Archived
feat: US #32 Section-based Reading Recommendations
Backend: - Add ReadingSchedule and MeetingSection models for ebook reading schedules - Implement recommendation algorithm: greedy partition minimizing per-meeting time variance - Algorithm respects chapter boundaries, estimates time based on chapter weight - Add serializers for schedules (list/detail/generate/confirm/update) - Add ScheduleViewSet with endpoints: list, create (generate), retrieve, regenerate, confirm, update_meeting, reorder_meetings - Add URL routes for /api/books/schedules/ - Create migration 0004 for new models Frontend: - Add types: MeetingSection, ReadingSchedule, GenerateRecommendationsRequest - Add schedulesApi with methods: getSchedules, generateRecommendations, confirmSchedule, updateMeeting, reorderMeetings, deleteSchedule - Build RecommendationPage with 4-column meeting layout, drag-and-drop chapter reassignment, generate/regenerate/confirm flow - Add 'Schedule Reading' menu item to BookContextMenu - Add route: /books/:ebookId/schedule
This commit is contained in:
@@ -2,7 +2,7 @@ from rest_framework import serializers
|
||||
|
||||
import logging
|
||||
|
||||
from apps.books.models import Book, BookChapter, EBook, ReadingProgress, ReadingStatus, DownloadRecord
|
||||
from apps.books.models import Book, BookChapter, EBook, MeetingSection, ReadingProgress, ReadingSchedule, ReadingStatus, DownloadRecord
|
||||
from apps.books.services.ebook_metadata import subjects_from_ebook
|
||||
from apps.books.services.metadata import enrich_ebook_metadata
|
||||
from apps.books.services.process_ebook import apply_processing_to_ebook
|
||||
@@ -228,4 +228,64 @@ class DownloadRecordSerializer(serializers.ModelSerializer):
|
||||
class StorageSummarySerializer(serializers.Serializer):
|
||||
total_downloads = serializers.IntegerField()
|
||||
total_size_bytes = serializers.IntegerField()
|
||||
ebooks = serializers.ListField(child=serializers.DictField())
|
||||
ebooks = serializers.ListField(child=serializers.DictField())
|
||||
|
||||
|
||||
class MeetingSectionSerializer(serializers.ModelSerializer):
|
||||
chapters = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = MeetingSection
|
||||
fields = [
|
||||
"id", "meeting_index", "title", "estimated_time_minutes",
|
||||
"chapter_ids", "chapters",
|
||||
]
|
||||
read_only_fields = ["id"]
|
||||
|
||||
def get_chapters(self, obj: MeetingSection) -> list[dict[str, str | int]]:
|
||||
"""Resolve chapter_ids to chapter title/index/href for the frontend."""
|
||||
if not obj.chapter_ids:
|
||||
return []
|
||||
chapters_qs = BookChapter.objects.filter(
|
||||
id__in=obj.chapter_ids,
|
||||
ebook=obj.schedule.ebook,
|
||||
).order_by("index").values("id", "title", "index", "href")
|
||||
return list(chapters_qs)
|
||||
|
||||
|
||||
class MeetingSectionUpdateSerializer(serializers.Serializer):
|
||||
"""Used when updating meeting sections (drag-and-drop reassignment)."""
|
||||
meeting_id = serializers.IntegerField(required=False)
|
||||
meeting_index = serializers.IntegerField(required=False)
|
||||
chapter_ids = serializers.ListField(child=serializers.IntegerField(), required=False)
|
||||
title = serializers.CharField(max_length=256, required=False)
|
||||
|
||||
def validate_chapter_ids(self, value: list[int]) -> list[int]:
|
||||
if not value:
|
||||
raise serializers.ValidationError("chapter_ids must not be empty.")
|
||||
return value
|
||||
|
||||
|
||||
class ReadingScheduleSerializer(serializers.ModelSerializer):
|
||||
meetings = MeetingSectionSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ReadingSchedule
|
||||
fields = [
|
||||
"id", "ebook", "meeting_count", "confirmed",
|
||||
"meetings", "created_at", "updated_at",
|
||||
]
|
||||
read_only_fields = ["id", "ebook", "created_at", "updated_at"]
|
||||
|
||||
|
||||
class GenerateRecommendationsSerializer(serializers.Serializer):
|
||||
meeting_count = serializers.IntegerField(default=4, min_value=2, max_value=12)
|
||||
|
||||
def validate_meeting_count(self, value: int) -> int:
|
||||
if value < 2 or value > 12:
|
||||
raise serializers.ValidationError("Meeting count must be between 2 and 12.")
|
||||
return value
|
||||
|
||||
|
||||
class ConfirmScheduleSerializer(serializers.Serializer):
|
||||
confirm = serializers.BooleanField(default=True)
|
||||
Reference in New Issue
Block a user