Archived
- Backend: Django REST Framework API with Bookmark and Note models - ViewSets with user-scoped querysets and select_related for N+1 prevention - Create/List/Detail/Update/Delete endpoints - Batch delete operations - Unique constraint on user+book+page for bookmarks - IsOwner permission class for object-level access control - Full serializer validation (page > 0, non-empty content, duplicate check) - 30+ pytest-django tests covering CRUD, auth, filtering, edge cases - Frontend: React TypeScript components - AnnotationsContext with useReducer for state management - BookmarkList, NoteList, AddAnnotationForm, AnnotationsDashboard - Inline note editing with immediate save - Batch delete support - API client with JWT auto-refresh interceptors - Paginated query hook for infinite scroll support - Responsive CSS with loading/empty states - Infrastructure: Django project with custom User model, JWT auth, CORS - PostgreSQL database models with proper FK and indexes - Django admin configuration for all models
108 lines
3.3 KiB
Python
108 lines
3.3 KiB
Python
from rest_framework import serializers
|
|
|
|
from apps.annotations.models import Bookmark, Note
|
|
|
|
|
|
class BookmarkSerializer(serializers.ModelSerializer):
|
|
"""Serialize Bookmark data with full details."""
|
|
|
|
book_title = serializers.CharField(source="book.title", read_only=True)
|
|
|
|
class Meta:
|
|
model = Bookmark
|
|
fields = [
|
|
"id",
|
|
"book",
|
|
"book_title",
|
|
"page",
|
|
"location_text",
|
|
"created_at",
|
|
"updated_at",
|
|
]
|
|
read_only_fields = ["id", "created_at", "updated_at", "book_title"]
|
|
|
|
def validate_page(self, value: int) -> int:
|
|
if value < 1:
|
|
raise serializers.ValidationError("Page must be a positive integer.")
|
|
return value
|
|
|
|
|
|
class BookmarkCreateSerializer(serializers.ModelSerializer):
|
|
"""Serializer used for creating bookmarks. Sets user from request context."""
|
|
|
|
class Meta:
|
|
model = Bookmark
|
|
fields = ["book", "page", "location_text"]
|
|
|
|
def validate_page(self, value: int) -> int:
|
|
if value < 1:
|
|
raise serializers.ValidationError("Page must be a positive integer.")
|
|
return value
|
|
|
|
def validate(self, attrs):
|
|
user = self.context["request"].user
|
|
if Bookmark.objects.filter(
|
|
user=user, book=attrs["book"], page=attrs["page"]
|
|
).exists():
|
|
raise serializers.ValidationError(
|
|
{"page": "A bookmark already exists at this page for this book."}
|
|
)
|
|
return attrs
|
|
|
|
def create(self, validated_data):
|
|
validated_data["user"] = self.context["request"].user
|
|
return super().create(validated_data)
|
|
|
|
|
|
class NoteSerializer(serializers.ModelSerializer):
|
|
"""Serialize Note data with full details."""
|
|
|
|
book_title = serializers.CharField(source="book.title", read_only=True)
|
|
|
|
class Meta:
|
|
model = Note
|
|
fields = [
|
|
"id",
|
|
"book",
|
|
"book_title",
|
|
"page",
|
|
"location_text",
|
|
"content",
|
|
"created_at",
|
|
"updated_at",
|
|
]
|
|
read_only_fields = ["id", "created_at", "updated_at", "book_title"]
|
|
|
|
def validate_page(self, value: int) -> int:
|
|
if value < 1:
|
|
raise serializers.ValidationError("Page must be a positive integer.")
|
|
return value
|
|
|
|
def validate_content(self, value: str) -> str:
|
|
stripped = value.strip()
|
|
if not stripped:
|
|
raise serializers.ValidationError("Note content cannot be empty.")
|
|
return stripped
|
|
|
|
|
|
class NoteCreateSerializer(serializers.ModelSerializer):
|
|
"""Serializer used for creating notes. Sets user from request context."""
|
|
|
|
class Meta:
|
|
model = Note
|
|
fields = ["book", "page", "location_text", "content"]
|
|
|
|
def validate_page(self, value: int) -> int:
|
|
if value < 1:
|
|
raise serializers.ValidationError("Page must be a positive integer.")
|
|
return value
|
|
|
|
def validate_content(self, value: str) -> str:
|
|
stripped = value.strip()
|
|
if not stripped:
|
|
raise serializers.ValidationError("Note content cannot be empty.")
|
|
return stripped
|
|
|
|
def create(self, validated_data):
|
|
validated_data["user"] = self.context["request"].user
|
|
return super().create(validated_data) |