From c537770172872fc87780e90c4353d8defa51c3b9 Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 18 Feb 2026 20:57:56 +0000 Subject: [PATCH] Decouple blog: use shared.models for all cross-app imports - Replace all imports from cart.models, market.models, events.models with shared.models equivalents - Convert blog/models/ghost_content.py to re-export stub - Update shared + glue submodule pointers Co-Authored-By: Claude Opus 4.6 --- bp/blog/ghost/ghost_sync.py | 2 +- bp/blog/ghost_db.py | 2 +- bp/blog/services/posts_data.py | 2 +- bp/post/admin/routes.py | 18 +- bp/post/routes.py | 4 +- bp/post/services/entry_associations.py | 2 +- bp/post/services/markets.py | 4 +- glue | 2 +- models/ghost_content.py | 217 +------------------------ shared | 2 +- 10 files changed, 21 insertions(+), 234 deletions(-) diff --git a/bp/blog/ghost/ghost_sync.py b/bp/blog/ghost/ghost_sync.py index 3c1e3b4..285b9c4 100644 --- a/bp/blog/ghost/ghost_sync.py +++ b/bp/blog/ghost/ghost_sync.py @@ -13,7 +13,7 @@ from sqlalchemy.orm.attributes import flag_modified # for non-Mutable JSON colu from models.ghost_content import ( Post, Author, Tag, PostAuthor, PostTag ) -from cart.models.page_config import PageConfig +from shared.models.page_config import PageConfig # User-centric membership models from shared.models import User diff --git a/bp/blog/ghost_db.py b/bp/blog/ghost_db.py index a058997..1e9eda6 100644 --- a/bp/blog/ghost_db.py +++ b/bp/blog/ghost_db.py @@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload, joinedload from models.ghost_content import Post, Author, Tag, PostTag -from cart.models.page_config import PageConfig +from shared.models.page_config import PageConfig from models.tag_group import TagGroup, TagGroupTag diff --git a/bp/blog/services/posts_data.py b/bp/blog/services/posts_data.py index 6034307..411b3cf 100644 --- a/bp/blog/services/posts_data.py +++ b/bp/blog/services/posts_data.py @@ -1,7 +1,7 @@ from ..ghost_db import DBClient # adjust import path from sqlalchemy import select from models.ghost_content import PostLike -from events.models.calendars import CalendarEntry, CalendarEntryPost +from shared.models.calendars import CalendarEntry, CalendarEntryPost from quart import g async def posts_data( diff --git a/bp/post/admin/routes.py b/bp/post/admin/routes.py index 9e51b62..8007456 100644 --- a/bp/post/admin/routes.py +++ b/bp/post/admin/routes.py @@ -22,7 +22,7 @@ def register(): @require_admin async def admin(slug: str): from shared.browser.app.utils.htmx import is_htmx_request - from cart.models.page_config import PageConfig + from shared.models.page_config import PageConfig from sqlalchemy import select as sa_select # Load features for page admin @@ -62,7 +62,7 @@ def register(): @require_admin async def update_features(slug: str): """Update PageConfig.features for a page.""" - from cart.models.page_config import PageConfig + from shared.models.page_config import PageConfig from models.ghost_content import Post from sqlalchemy import select as sa_select from quart import jsonify @@ -129,7 +129,7 @@ def register(): @require_admin async def update_sumup(slug: str): """Update PageConfig SumUp credentials for a page.""" - from cart.models.page_config import PageConfig + from shared.models.page_config import PageConfig from sqlalchemy import select as sa_select from quart import jsonify @@ -191,7 +191,7 @@ def register(): @require_admin async def calendar_view(slug: str, calendar_id: int): """Show calendar month view for browsing entries""" - from events.models.calendars import Calendar + from shared.models.calendars import Calendar from sqlalchemy import select from datetime import datetime, timezone from quart import request @@ -273,7 +273,7 @@ def register(): @require_admin async def entries(slug: str): from ..services.entry_associations import get_post_entry_ids - from events.models.calendars import Calendar + from shared.models.calendars import Calendar from sqlalchemy import select post_id = g.post_data["post"]["id"] @@ -309,7 +309,7 @@ def register(): @require_admin async def toggle_entry(slug: str, entry_id: int): from ..services.entry_associations import toggle_entry_association, get_post_entry_ids, get_associated_entries - from events.models.calendars import Calendar + from shared.models.calendars import Calendar from sqlalchemy import select from quart import jsonify @@ -603,7 +603,7 @@ def register(): @require_admin async def markets(slug: str): """List markets for this page.""" - from market.models.market_place import MarketPlace + from shared.models.market_place import MarketPlace from sqlalchemy import select as sa_select post = (g.post_data or {}).get("post", {}) @@ -631,7 +631,7 @@ def register(): async def create_market(slug: str): """Create a new market for this page.""" from ..services.markets import create_market as _create_market, MarketError - from market.models.market_place import MarketPlace + from shared.models.market_place import MarketPlace from sqlalchemy import select as sa_select from quart import jsonify @@ -669,7 +669,7 @@ def register(): async def delete_market(slug: str, market_slug: str): """Soft-delete a market.""" from ..services.markets import soft_delete_market - from market.models.market_place import MarketPlace + from shared.models.market_place import MarketPlace from sqlalchemy import select as sa_select from quart import jsonify diff --git a/bp/post/routes.py b/bp/post/routes.py index ea8679e..9bccc63 100644 --- a/bp/post/routes.py +++ b/bp/post/routes.py @@ -11,8 +11,8 @@ from quart import ( ) from .services.post_data import post_data from .services.post_operations import toggle_post_like -from events.models.calendars import Calendar -from market.models.market_place import MarketPlace +from shared.models.calendars import Calendar +from shared.models.market_place import MarketPlace from sqlalchemy import select from shared.browser.app.redis_cacher import cache_page, clear_cache diff --git a/bp/post/services/entry_associations.py b/bp/post/services/entry_associations.py index af387a4..3603960 100644 --- a/bp/post/services/entry_associations.py +++ b/bp/post/services/entry_associations.py @@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from sqlalchemy.sql import func -from events.models.calendars import CalendarEntry, CalendarEntryPost, Calendar +from shared.models.calendars import CalendarEntry, CalendarEntryPost, Calendar from models.ghost_content import Post diff --git a/bp/post/services/markets.py b/bp/post/services/markets.py index a28989d..66628f3 100644 --- a/bp/post/services/markets.py +++ b/bp/post/services/markets.py @@ -6,9 +6,9 @@ import unicodedata from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from market.models.market_place import MarketPlace +from shared.models.market_place import MarketPlace from models.ghost_content import Post -from cart.models.page_config import PageConfig +from shared.models.page_config import PageConfig from shared.browser.app.utils import utcnow from glue.services.relationships import attach_child, detach_child diff --git a/glue b/glue index fc14d83..ebce44e 160000 --- a/glue +++ b/glue @@ -1 +1 @@ -Subproject commit fc14d8323adcb404894c8ab875431c9d571cfabb +Subproject commit ebce44e9d9ae0938647290a1e98687ae79fd71eb diff --git a/models/ghost_content.py b/models/ghost_content.py index 197f651..cd18161 100644 --- a/models/ghost_content.py +++ b/models/ghost_content.py @@ -1,216 +1,3 @@ -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.models.ghost_content import ( # noqa: F401 + Tag, Post, Author, PostAuthor, PostTag, PostLike, ) -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[str] = mapped_column(String(64), index=True, unique=True, nullable=False) - - 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[str] = mapped_column(String(64), index=True, unique=True, nullable=False) - uuid: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) - 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()) - - 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)) - created_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) - deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) - - user_id: Mapped[Optional[int]] = mapped_column( - Integer, ForeignKey("users.id", ondelete="SET NULL"), 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] - ) - user: Mapped[Optional["User"]] = relationship( - "User", foreign_keys=[user_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", - ) - likes: Mapped[List["PostLike"]] = relationship( - "PostLike", - back_populates="post", - cascade="all, delete-orphan", - passive_deletes=True, - ) - -class Author(Base): - __tablename__ = "authors" - - id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - ghost_id: Mapped[str] = mapped_column(String(64), index=True, unique=True, nullable=False) - - 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 PostLike(Base): - __tablename__ = "post_likes" - - id = Column(Integer, primary_key=True, autoincrement=True) - user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) - post_id: Mapped[int] = mapped_column(ForeignKey("posts.id", ondelete="CASCADE"), nullable=False) - - 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()) - deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) - - post: Mapped["Post"] = relationship("Post", back_populates="likes", foreign_keys=[post_id]) - user = relationship("User", back_populates="liked_posts") diff --git a/shared b/shared index da10fc4..0c0f3c8 160000 --- a/shared +++ b/shared @@ -1 +1 @@ -Subproject commit da10fc4cf91d355342decb2c47e4d4e279e981a1 +Subproject commit 0c0f3c84167ff61ad469e1f5ad93de7841c59e76