Draft/published article library with stable section IDs, breadcrumbs, link remediation, question and card associations, manual card creation and side-by-side/mobile-drawer reading. Migration e8d4f1a27c93. Verified 42 deployed-image backend tests, 72 frontend tests/build and PostgreSQL migration round-trip.
45 lines
2.2 KiB
Python
45 lines
2.2 KiB
Python
from datetime import datetime
|
|
|
|
from sqlalchemy import Column, Integer, String, Text, JSON, DateTime, ForeignKey, UniqueConstraint
|
|
from sqlalchemy.orm import relationship
|
|
|
|
from app.database import Base
|
|
|
|
|
|
class Article(Base):
|
|
"""Educator-authored topic reading with stable section IDs for linking."""
|
|
|
|
__tablename__ = "articles"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
slug = Column(String(120), unique=True, nullable=False, index=True)
|
|
title = Column(String(300), nullable=False)
|
|
summary = Column(Text, nullable=True)
|
|
content = Column(Text, nullable=True) # Whole-article introduction (markdown).
|
|
# Stable subsections: [{"id": uuid-hex, "slug": "...", "title": "...", "content": "markdown"}].
|
|
sections = Column(JSON, nullable=False, default=list)
|
|
category_id = Column(Integer, ForeignKey("question_categories.id", ondelete="SET NULL"), nullable=True)
|
|
section_id = Column(Integer, ForeignKey("sections.id", ondelete="SET NULL"), nullable=True) # Optional PDF source range.
|
|
user_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
|
status = Column(String, default="draft") # draft | published
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
category = relationship("QuestionCategory")
|
|
links = relationship("QuestionArticleLink", back_populates="article", cascade="all, delete-orphan")
|
|
|
|
|
|
class QuestionArticleLink(Base):
|
|
"""Question -> whole article or one stable section."""
|
|
|
|
__tablename__ = "question_article_links"
|
|
__table_args__ = (UniqueConstraint("question_id", "article_id", "section_id", name="uq_question_article_section"),)
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
question_id = Column(Integer, ForeignKey("questions.id", ondelete="CASCADE"), nullable=False)
|
|
article_id = Column(Integer, ForeignKey("articles.id", ondelete="CASCADE"), nullable=False)
|
|
section_id = Column(String(64), nullable=True) # Article.sections[].id; None = whole article.
|
|
user_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
|
|
article = relationship("Article", back_populates="links")
|