"""SeriesChapter — an ordered chapter/part within a series. A series IS a Tag(kind='series'); a chapter groups ordered SeriesPages under it. Reading order is (chapter.chapter_number, series_page.page_number): chapter_number sets the order of chapters, page_number orders pages within a chapter. chapter_number is an ordering key only (not unique) — reorder rewrites 1..N wholesale, mirroring series_page.page_number, so a reorder can't transiently collide on a unique index. A chapter may be a placeholder (is_placeholder=True) — a reserved empty slot for a section the operator doesn't have yet; it holds no pages and shows as a gap in the reader. stated_page_start/end carry the page range parsed from the source post (FC-6.2), used to flag missing-page gaps; both are nullable when unknown. stated_part is the operator-facing "Part N" label (FC-6.4), separate from the positional chapter_number: chapter_number is auto-managed ordering (rewritten 1..N on reorder/delete), while stated_part is the real installment number the operator types — e.g. a series authored from a post that is Part 2 of a story. Nullable when unset (the UI then falls back to showing chapter_number). """ from datetime import datetime from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, Text, func from sqlalchemy.orm import Mapped, mapped_column from .base import Base class SeriesChapter(Base): __tablename__ = "series_chapter" id: Mapped[int] = mapped_column(Integer, primary_key=True) series_tag_id: Mapped[int] = mapped_column( ForeignKey("tag.id", ondelete="CASCADE"), nullable=False, index=True ) chapter_number: Mapped[int] = mapped_column(Integer, nullable=False) stated_part: Mapped[int | None] = mapped_column(Integer, nullable=True) title: Mapped[str | None] = mapped_column(Text, nullable=True) is_placeholder: Mapped[bool] = mapped_column( Boolean, nullable=False, server_default="false" ) stated_page_start: Mapped[int | None] = mapped_column(Integer, nullable=True) stated_page_end: Mapped[int | None] = mapped_column(Integer, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now(), )