Archived
- Backend: Chapter, ReadingProgress, ReadingSettings models - Backend: Chapter API (TOC + content), progress tracking, settings CRUD - Frontend: ReadingPage with chapter navigation - Frontend: TableOfContents drawer - Frontend: ReadingSettingsPanel (theme, font, size, orientation) - Frontend: Custom hooks for settings, chapters, progress tracking - CSS: Mobile-first reading view with sepia/dark/light/paper themes - Route: /reader/:bookId reading view from book detail page - Docs: 001-customizable-mobile-reading-experience.md
75 lines
2.9 KiB
Python
75 lines
2.9 KiB
Python
from django_filters.rest_framework import DjangoFilterBackend
|
|
from rest_framework import status, viewsets
|
|
from rest_framework.decorators import action
|
|
from rest_framework.filters import OrderingFilter, SearchFilter
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.request import Request
|
|
from rest_framework.response import Response
|
|
|
|
from apps.books.models import Book, Chapter, ReadingProgress
|
|
from apps.books.serializers import (
|
|
BookListSerializer,
|
|
BookSerializer,
|
|
ChapterDetailSerializer,
|
|
ChapterSummarySerializer,
|
|
ReadingProgressSerializer,
|
|
)
|
|
|
|
|
|
class BookViewSet(viewsets.ModelViewSet):
|
|
"""CRUD for books. Includes nested chapter and progress endpoints."""
|
|
|
|
queryset = Book.objects.all().prefetch_related("chapters")
|
|
permission_classes = [IsAuthenticated]
|
|
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
|
filterset_fields = ["author"]
|
|
search_fields = ["title", "author"]
|
|
ordering_fields = ["title", "author", "created_at"]
|
|
ordering = ["title"]
|
|
|
|
def get_serializer_class(self):
|
|
if self.action == "list":
|
|
return BookListSerializer
|
|
return BookSerializer
|
|
|
|
@action(detail=True, methods=["get"])
|
|
def chapters(self, request: Request, pk: int | None = None) -> Response:
|
|
"""List all chapters for this book (TOC)."""
|
|
book = self.get_object()
|
|
chapters = book.chapters.all().order_by("number")
|
|
serializer = ChapterSummarySerializer(chapters, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=True, methods=["get"], url_path="chapters/(?P<chapter_number>[0-9]+)")
|
|
def chapter_detail(
|
|
self, request: Request, pk: int | None = None, chapter_number: str | None = None
|
|
) -> Response:
|
|
"""Get a specific chapter with full content."""
|
|
book = self.get_object()
|
|
try:
|
|
chapter = book.chapters.get(number=int(chapter_number))
|
|
except Chapter.DoesNotExist:
|
|
return Response(
|
|
{"detail": "Chapter not found."}, status=status.HTTP_404_NOT_FOUND
|
|
)
|
|
serializer = ChapterDetailSerializer(chapter)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=True, methods=["get", "put"])
|
|
def progress(self, request: Request, pk: int | None = None) -> Response:
|
|
"""Get or update reading progress for this book."""
|
|
book = self.get_object()
|
|
progress, created = ReadingProgress.objects.get_or_create(
|
|
user=request.user, book=book
|
|
)
|
|
|
|
if request.method == "GET":
|
|
serializer = ReadingProgressSerializer(progress)
|
|
return Response(serializer.data)
|
|
|
|
serializer = ReadingProgressSerializer(
|
|
progress, data=request.data, partial=True
|
|
)
|
|
serializer.is_valid(raise_exception=True)
|
|
serializer.save()
|
|
return Response(serializer.data, status=status.HTTP_200_OK) |