Archived
feat: full book management system with backend API, frontend UI, and spec docs
- 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
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ReadingStatus(models.TextChoices):
|
||||
"""Enumeration of possible reading statuses for a book."""
|
||||
|
||||
NOT_STARTED = "not_started", _("Not Started")
|
||||
READING = "reading", _("Reading")
|
||||
FINISHED = "finished", _("Finished")
|
||||
DNF = "dnf", _("Did Not Finish")
|
||||
|
||||
|
||||
class Book(models.Model):
|
||||
"""Represents a book in the user's library."""
|
||||
|
||||
title = models.CharField(max_length=500, verbose_name=_("Title"))
|
||||
author = models.CharField(max_length=500, verbose_name=_("Author"))
|
||||
genre = models.CharField(max_length=200, blank=True, default="", verbose_name=_("Genre"))
|
||||
description = models.TextField(blank=True, default="", verbose_name=_("Description"))
|
||||
cover_image_url = models.URLField(blank=True, default="", verbose_name=_("Cover Image URL"))
|
||||
isbn = models.CharField(max_length=20, blank=True, default="", verbose_name=_("ISBN"))
|
||||
total_pages = models.PositiveIntegerField(default=0, verbose_name=_("Total Pages"))
|
||||
current_page = models.PositiveIntegerField(default=0, verbose_name=_("Current Page"))
|
||||
reading_status = models.CharField(
|
||||
max_length=20,
|
||||
choices=ReadingStatus.choices,
|
||||
default=ReadingStatus.NOT_STARTED,
|
||||
verbose_name=_("Reading Status"),
|
||||
)
|
||||
owner = models.ForeignKey(
|
||||
"auth.User",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="books",
|
||||
verbose_name=_("Owner"),
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
|
||||
|
||||
class Meta:
|
||||
ordering = ["-updated_at"]
|
||||
verbose_name = _("Book")
|
||||
verbose_name_plural = _("Books")
|
||||
indexes = [
|
||||
models.Index(fields=["owner", "reading_status"]),
|
||||
models.Index(fields=["owner", "title"]),
|
||||
models.Index(fields=["owner", "author"]),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.title} by {self.author}"
|
||||
|
||||
@property
|
||||
def reading_progress(self) -> float:
|
||||
"""Calculate reading progress as a percentage (0.0 - 100.0)."""
|
||||
if self.total_pages == 0:
|
||||
return 0.0
|
||||
return round((self.current_page / self.total_pages) * 100, 1)
|
||||
|
||||
def mark_as_finished(self) -> None:
|
||||
"""Mark the book as finished, setting progress to 100%."""
|
||||
self.reading_status = ReadingStatus.FINISHED
|
||||
self.current_page = self.total_pages
|
||||
self.save(update_fields=["reading_status", "current_page", "updated_at"])
|
||||
Reference in New Issue
Block a user