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
66 lines
2.6 KiB
Python
66 lines
2.6 KiB
Python
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"]) |