Model: add sx_content column to Post. Writer: accept sx_content in create_post, create_page, update_post. Routes: read sx_content from form data in new post, new page, and edit routes. Read pipeline: ghost_db includes sx_content in public dict, detail/home views prefer sx_content over html when available, PostDTO includes sx_content. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
208 lines
8.4 KiB
Python
208 lines
8.4 KiB
Python
from datetime import datetime
|
|
from typing import List, Optional
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
from sqlalchemy import (
|
|
Integer,
|
|
String,
|
|
Text,
|
|
Boolean,
|
|
DateTime,
|
|
ForeignKey,
|
|
Column,
|
|
func,
|
|
)
|
|
from shared.db.base import Base # whatever your Base is
|
|
# from .author import Author # make sure imports resolve
|
|
# from ..app.blog.calendars.model import Calendar
|
|
|
|
class Tag(Base):
|
|
__tablename__ = "tags"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
ghost_id: Mapped[Optional[str]] = mapped_column(String(64), index=True, unique=True, nullable=True)
|
|
|
|
slug: Mapped[str] = mapped_column(String(191), index=True, nullable=False)
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
|
|
description: Mapped[Optional[str]] = mapped_column(Text())
|
|
visibility: Mapped[str] = mapped_column(String(32), default="public", nullable=False)
|
|
feature_image: Mapped[Optional[str]] = mapped_column(Text())
|
|
|
|
meta_title: Mapped[Optional[str]] = mapped_column(String(300))
|
|
meta_description: Mapped[Optional[str]] = mapped_column(Text())
|
|
|
|
created_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
|
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
|
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
|
|
|
# NEW: posts relationship is now direct Post objects via PostTag
|
|
posts: Mapped[List["Post"]] = relationship(
|
|
"Post",
|
|
secondary="post_tags",
|
|
primaryjoin="Tag.id==post_tags.c.tag_id",
|
|
secondaryjoin="Post.id==post_tags.c.post_id",
|
|
back_populates="tags",
|
|
order_by="PostTag.sort_order",
|
|
)
|
|
|
|
|
|
class Post(Base):
|
|
__tablename__ = "posts"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
ghost_id: Mapped[Optional[str]] = mapped_column(String(64), index=True, unique=True, nullable=True)
|
|
uuid: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, server_default=func.gen_random_uuid())
|
|
slug: Mapped[str] = mapped_column(String(191), index=True, nullable=False)
|
|
|
|
title: Mapped[str] = mapped_column(String(500), nullable=False)
|
|
|
|
html: Mapped[Optional[str]] = mapped_column(Text())
|
|
plaintext: Mapped[Optional[str]] = mapped_column(Text())
|
|
mobiledoc: Mapped[Optional[str]] = mapped_column(Text())
|
|
lexical: Mapped[Optional[str]] = mapped_column(Text())
|
|
sx_content: Mapped[Optional[str]] = mapped_column(Text())
|
|
|
|
feature_image: Mapped[Optional[str]] = mapped_column(Text())
|
|
feature_image_alt: Mapped[Optional[str]] = mapped_column(Text())
|
|
feature_image_caption: Mapped[Optional[str]] = mapped_column(Text())
|
|
|
|
excerpt: Mapped[Optional[str]] = mapped_column(Text())
|
|
custom_excerpt: Mapped[Optional[str]] = mapped_column(Text())
|
|
|
|
visibility: Mapped[str] = mapped_column(String(32), default="public", nullable=False)
|
|
status: Mapped[str] = mapped_column(String(32), default="draft", nullable=False)
|
|
featured: Mapped[bool] = mapped_column(Boolean(), default=False, nullable=False)
|
|
is_page: Mapped[bool] = mapped_column(Boolean(), default=False, nullable=False)
|
|
email_only: Mapped[bool] = mapped_column(Boolean(), default=False, nullable=False)
|
|
|
|
canonical_url: Mapped[Optional[str]] = mapped_column(Text())
|
|
meta_title: Mapped[Optional[str]] = mapped_column(String(500))
|
|
meta_description: Mapped[Optional[str]] = mapped_column(Text())
|
|
og_image: Mapped[Optional[str]] = mapped_column(Text())
|
|
og_title: Mapped[Optional[str]] = mapped_column(String(500))
|
|
og_description: Mapped[Optional[str]] = mapped_column(Text())
|
|
twitter_image: Mapped[Optional[str]] = mapped_column(Text())
|
|
twitter_title: Mapped[Optional[str]] = mapped_column(String(500))
|
|
twitter_description: Mapped[Optional[str]] = mapped_column(Text())
|
|
custom_template: Mapped[Optional[str]] = mapped_column(String(191))
|
|
|
|
reading_time: Mapped[Optional[int]] = mapped_column(Integer())
|
|
comment_id: Mapped[Optional[str]] = mapped_column(String(191))
|
|
|
|
published_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
|
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
|
created_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
|
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
|
|
|
user_id: Mapped[Optional[int]] = mapped_column(
|
|
Integer, index=True
|
|
)
|
|
publish_requested: Mapped[bool] = mapped_column(Boolean(), default=False, server_default="false", nullable=False)
|
|
|
|
primary_author_id: Mapped[Optional[int]] = mapped_column(
|
|
Integer, ForeignKey("authors.id", ondelete="SET NULL")
|
|
)
|
|
primary_tag_id: Mapped[Optional[int]] = mapped_column(
|
|
Integer, ForeignKey("tags.id", ondelete="SET NULL")
|
|
)
|
|
|
|
primary_author: Mapped[Optional["Author"]] = relationship(
|
|
"Author", foreign_keys=[primary_author_id]
|
|
)
|
|
primary_tag: Mapped[Optional[Tag]] = relationship(
|
|
"Tag", foreign_keys=[primary_tag_id]
|
|
)
|
|
|
|
# AUTHORS RELATIONSHIP (many-to-many via post_authors)
|
|
authors: Mapped[List["Author"]] = relationship(
|
|
"Author",
|
|
secondary="post_authors",
|
|
primaryjoin="Post.id==post_authors.c.post_id",
|
|
secondaryjoin="Author.id==post_authors.c.author_id",
|
|
back_populates="posts",
|
|
order_by="PostAuthor.sort_order",
|
|
)
|
|
|
|
# TAGS RELATIONSHIP (many-to-many via post_tags)
|
|
tags: Mapped[List[Tag]] = relationship(
|
|
"Tag",
|
|
secondary="post_tags",
|
|
primaryjoin="Post.id==post_tags.c.post_id",
|
|
secondaryjoin="Tag.id==post_tags.c.tag_id",
|
|
back_populates="posts",
|
|
order_by="PostTag.sort_order",
|
|
)
|
|
|
|
class Author(Base):
|
|
__tablename__ = "authors"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
ghost_id: Mapped[Optional[str]] = mapped_column(String(64), index=True, unique=True, nullable=True)
|
|
|
|
slug: Mapped[str] = mapped_column(String(191), index=True, nullable=False)
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
email: Mapped[Optional[str]] = mapped_column(String(255))
|
|
|
|
profile_image: Mapped[Optional[str]] = mapped_column(Text())
|
|
cover_image: Mapped[Optional[str]] = mapped_column(Text())
|
|
bio: Mapped[Optional[str]] = mapped_column(Text())
|
|
website: Mapped[Optional[str]] = mapped_column(Text())
|
|
location: Mapped[Optional[str]] = mapped_column(Text())
|
|
facebook: Mapped[Optional[str]] = mapped_column(Text())
|
|
twitter: Mapped[Optional[str]] = mapped_column(Text())
|
|
|
|
created_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
|
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
|
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
|
|
|
# backref to posts via post_authors
|
|
posts: Mapped[List[Post]] = relationship(
|
|
"Post",
|
|
secondary="post_authors",
|
|
primaryjoin="Author.id==post_authors.c.author_id",
|
|
secondaryjoin="Post.id==post_authors.c.post_id",
|
|
back_populates="authors",
|
|
order_by="PostAuthor.sort_order",
|
|
)
|
|
|
|
class PostAuthor(Base):
|
|
__tablename__ = "post_authors"
|
|
|
|
post_id: Mapped[int] = mapped_column(
|
|
ForeignKey("posts.id", ondelete="CASCADE"),
|
|
primary_key=True,
|
|
)
|
|
author_id: Mapped[int] = mapped_column(
|
|
ForeignKey("authors.id", ondelete="CASCADE"),
|
|
primary_key=True,
|
|
)
|
|
sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
|
|
|
|
class PostTag(Base):
|
|
__tablename__ = "post_tags"
|
|
|
|
post_id: Mapped[int] = mapped_column(
|
|
ForeignKey("posts.id", ondelete="CASCADE"),
|
|
primary_key=True,
|
|
)
|
|
tag_id: Mapped[int] = mapped_column(
|
|
ForeignKey("tags.id", ondelete="CASCADE"),
|
|
primary_key=True,
|
|
)
|
|
sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
|
|
|
|
class PostUser(Base):
|
|
"""Multi-author M2M: links posts to users (cross-DB, no FK on user_id)."""
|
|
__tablename__ = "post_users"
|
|
|
|
post_id: Mapped[int] = mapped_column(
|
|
ForeignKey("posts.id", ondelete="CASCADE"),
|
|
primary_key=True,
|
|
)
|
|
user_id: Mapped[int] = mapped_column(
|
|
Integer, primary_key=True, index=True,
|
|
)
|
|
sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|