Archived
- Backend: Book model with reading progress, DRF ViewSet with full CRUD, search, sort, filter, pagination, mark-as-finished, stats endpoint - Frontend: Library grid, BookCard, BookDetail, BookForm components with React 19 + TypeScript + Vite - Tests: 29 passing tests covering models, API, serializers, permissions - Spec: backend api-spec.md and frontend component-spec.md in docs/ Closes crisleo-hermes/cloud-reader#3
95 lines
3.0 KiB
Python
95 lines
3.0 KiB
Python
from __future__ import annotations
|
|
|
|
from rest_framework import serializers
|
|
|
|
from books.models import Book, ReadingStatus
|
|
|
|
|
|
class BookListSerializer(serializers.ModelSerializer):
|
|
"""Lightweight serializer for list views — avoids heavy field serialization."""
|
|
|
|
reading_progress = serializers.FloatField(read_only=True)
|
|
|
|
class Meta:
|
|
model = Book
|
|
fields = [
|
|
"id",
|
|
"title",
|
|
"author",
|
|
"genre",
|
|
"reading_status",
|
|
"reading_progress",
|
|
"cover_image_url",
|
|
"created_at",
|
|
"updated_at",
|
|
]
|
|
|
|
|
|
class BookDetailSerializer(serializers.ModelSerializer):
|
|
"""Full serializer for book detail views including all metadata."""
|
|
|
|
reading_progress = serializers.FloatField(read_only=True)
|
|
owner = serializers.ReadOnlyField(source="owner.username")
|
|
|
|
class Meta:
|
|
model = Book
|
|
fields = [
|
|
"id",
|
|
"title",
|
|
"author",
|
|
"genre",
|
|
"description",
|
|
"cover_image_url",
|
|
"isbn",
|
|
"total_pages",
|
|
"current_page",
|
|
"reading_status",
|
|
"reading_progress",
|
|
"owner",
|
|
"created_at",
|
|
"updated_at",
|
|
]
|
|
read_only_fields = ["owner", "created_at", "updated_at", "reading_progress"]
|
|
|
|
def validate_title(self, value: str) -> str:
|
|
"""Ensure title is not just whitespace."""
|
|
stripped = value.strip()
|
|
if not stripped:
|
|
msg = "Title cannot be empty."
|
|
raise serializers.ValidationError(msg)
|
|
return stripped
|
|
|
|
def validate_author(self, value: str) -> str:
|
|
"""Ensure author is not just whitespace."""
|
|
stripped = value.strip()
|
|
if not stripped:
|
|
msg = "Author cannot be empty."
|
|
raise serializers.ValidationError(msg)
|
|
return stripped
|
|
|
|
def validate(self, attrs: dict) -> dict:
|
|
"""Business rules — handles both full creates and partial updates."""
|
|
# Resolve effective values: use provided attrs, fall back to instance
|
|
current_total = getattr(self.instance, "total_pages", None)
|
|
current_current = getattr(self.instance, "current_page", None)
|
|
total_pages = attrs.get("total_pages", current_total) or 0
|
|
current_page = attrs.get("current_page", current_current) or 0
|
|
reading_status = attrs.get("reading_status", None)
|
|
|
|
if current_page > total_pages > 0:
|
|
msg = "Current page cannot exceed total pages."
|
|
raise serializers.ValidationError({"current_page": msg})
|
|
|
|
if reading_status == ReadingStatus.FINISHED:
|
|
if total_pages > 0:
|
|
attrs["current_page"] = total_pages
|
|
elif self.instance and self.instance.total_pages > 0:
|
|
attrs["current_page"] = self.instance.total_pages
|
|
|
|
return attrs
|
|
|
|
|
|
class BookWriteSerializer(BookDetailSerializer):
|
|
"""Alias for detail serializer — used for write operations with full validation."""
|
|
|
|
pass |