Archived
Address Reid's PR #25 review comments
🔴 Blocking fixes: - Add reader app migration (apps/reader/migrations/0001_initial.py) - Remove duplicate ReadingSettings from apps.books to resolve model clash with apps.reader.ReadingSettings (keep richer reader version) - Redirect books serializers/views to use reader app's ReadingSettings - Update frontend API endpoints to match main's backend routes: /api/books/ebooks/{id}/toc/ (chapters) /api/books/ebooks/{id}/content/?page=N (chapter content) /api/books/ebooks/{id}/progress/ (GET/PATCH progress) - Add DOMPurify sanitization for dangerouslySetInnerHTML (XSS fix) 🟡 Non-blocking fixes: - Fix CSS typo: landascape -> landscape in reader.css - Add null-safe fallback for book.title in ReadingPage
This commit is contained in:
+64
-25
@@ -12,12 +12,12 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from apps.books.models import Book, BookChapter, EBook, ReadingProgress, ReadingSettings
|
||||
from apps.books.models import Book, BookChapter, DownloadRecord, EBook, ReadingProgress
|
||||
from apps.books.serializers import (
|
||||
BookDetailSerializer, BookListSerializer, BookSerializer,
|
||||
BookChapterSerializer, EBookContentSerializer, EBookDetailSerializer,
|
||||
BookChapterSerializer, BookDetailSerializer, BookListSerializer, BookSerializer,
|
||||
DownloadRecordSerializer, EBookContentSerializer, EBookDetailSerializer,
|
||||
EBookListSerializer, EBookTocSerializer, EBookUploadSerializer,
|
||||
ReadingProgressSerializer, ReadingSettingsSerializer,
|
||||
ReadingProgressSerializer, StorageSummarySerializer,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -56,6 +56,23 @@ class BookViewSet(viewsets.ModelViewSet):
|
||||
author_list = Book.objects.values_list("author", flat=True).distinct().order_by("author")
|
||||
return Response([a for a in author_list if a])
|
||||
|
||||
@action(detail=False, methods=["get"])
|
||||
def storage(self, request: Request) -> Response:
|
||||
"""Return storage usage summary for the current user."""
|
||||
download_records = DownloadRecord.objects.filter(user=request.user).select_related("ebook")
|
||||
total_size = sum(r.file_size for r in download_records)
|
||||
ebook_list = [
|
||||
{"id": r.ebook.id, "title": r.ebook.title, "file_size": r.file_size}
|
||||
for r in download_records
|
||||
]
|
||||
serializer = StorageSummarySerializer(data={
|
||||
"total_downloads": download_records.count(),
|
||||
"total_size_bytes": total_size,
|
||||
"ebooks": ebook_list,
|
||||
})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class IsEBookOwner(permissions.BasePermission):
|
||||
def has_object_permission(self, request: Request, view: object, obj: EBook) -> bool:
|
||||
@@ -169,6 +186,48 @@ class EBookViewSet(viewsets.ModelViewSet):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=["post"])
|
||||
def download(self, request: Request, pk: int | None = None) -> Response:
|
||||
"""Track download of an e-book. Creates a DownloadRecord and returns file info."""
|
||||
ebook = self.get_object()
|
||||
if not ebook.file:
|
||||
return Response({"error": "No file found for this e-book."}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
download, created = DownloadRecord.objects.get_or_create(
|
||||
user=request.user,
|
||||
ebook=ebook,
|
||||
defaults={"file_size": ebook.file.size if ebook.file else 0},
|
||||
)
|
||||
if not created:
|
||||
download.file_size = ebook.file.size if ebook.file else 0
|
||||
download.save(update_fields=["file_size"])
|
||||
|
||||
serializer = DownloadRecordSerializer(download, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=False, methods=["get"])
|
||||
def downloads(self, request: Request) -> Response:
|
||||
"""List all e-books the current user has downloaded."""
|
||||
records = DownloadRecord.objects.filter(user=request.user).select_related(
|
||||
"ebook", "ebook__reading_progress"
|
||||
).prefetch_related("ebook__chapters")
|
||||
page = self.paginate_queryset(records)
|
||||
if page is not None:
|
||||
serializer = DownloadRecordSerializer(page, many=True, context={"request": request})
|
||||
return self.get_paginated_response(serializer.data)
|
||||
serializer = DownloadRecordSerializer(records, many=True, context={"request": request})
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=["delete"], url_path="downloads/(?P<download_pk>[^/.]+)")
|
||||
def delete_download(self, request: Request, download_pk: str | None = None) -> Response:
|
||||
"""Delete a download record."""
|
||||
try:
|
||||
download = DownloadRecord.objects.get(pk=download_pk, user=request.user)
|
||||
except DownloadRecord.DoesNotExist:
|
||||
return Response({"error": "Download record not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
download.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
def _store_chapters(ebook: EBook, toc: list[dict[str, Any]], parent_index: int = 0) -> None:
|
||||
"""Recursively store TOC entries as BookChapter records."""
|
||||
@@ -217,24 +276,4 @@ def _fetch_epub_chapter_content(file_path: str, chapter: BookChapter) -> str:
|
||||
return ""
|
||||
except Exception:
|
||||
logger.exception("Failed to fetch EPUB chapter content for %s", chapter.href)
|
||||
return ""
|
||||
|
||||
|
||||
class ReadingSettingsViewSet(viewsets.GenericViewSet):
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = ReadingSettingsSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return ReadingSettings.objects.filter(user=self.request.user)
|
||||
|
||||
def list(self, request: Request) -> Response:
|
||||
settings_obj, _created = ReadingSettings.objects.get_or_create(user=request.user)
|
||||
serializer = self.get_serializer(settings_obj)
|
||||
return Response(serializer.data)
|
||||
|
||||
def partial_update(self, request: Request) -> Response:
|
||||
settings_obj, _created = ReadingSettings.objects.get_or_create(user=request.user)
|
||||
serializer = self.get_serializer(settings_obj, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
return ""
|
||||
Reference in New Issue
Block a user