Decouple blog: use shared.models for all cross-app imports
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 47s

- 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 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-18 20:57:56 +00:00
parent 1ea2950310
commit c537770172
10 changed files with 21 additions and 234 deletions

View File

@@ -13,7 +13,7 @@ from sqlalchemy.orm.attributes import flag_modified # for non-Mutable JSON colu
from models.ghost_content import ( from models.ghost_content import (
Post, Author, Tag, PostAuthor, PostTag Post, Author, Tag, PostAuthor, PostTag
) )
from cart.models.page_config import PageConfig from shared.models.page_config import PageConfig
# User-centric membership models # User-centric membership models
from shared.models import User from shared.models import User

View File

@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload, joinedload from sqlalchemy.orm import selectinload, joinedload
from models.ghost_content import Post, Author, Tag, PostTag 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 from models.tag_group import TagGroup, TagGroupTag

View File

@@ -1,7 +1,7 @@
from ..ghost_db import DBClient # adjust import path from ..ghost_db import DBClient # adjust import path
from sqlalchemy import select from sqlalchemy import select
from models.ghost_content import PostLike from models.ghost_content import PostLike
from events.models.calendars import CalendarEntry, CalendarEntryPost from shared.models.calendars import CalendarEntry, CalendarEntryPost
from quart import g from quart import g
async def posts_data( async def posts_data(

View File

@@ -22,7 +22,7 @@ def register():
@require_admin @require_admin
async def admin(slug: str): async def admin(slug: str):
from shared.browser.app.utils.htmx import is_htmx_request 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 from sqlalchemy import select as sa_select
# Load features for page admin # Load features for page admin
@@ -62,7 +62,7 @@ def register():
@require_admin @require_admin
async def update_features(slug: str): async def update_features(slug: str):
"""Update PageConfig.features for a page.""" """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 models.ghost_content import Post
from sqlalchemy import select as sa_select from sqlalchemy import select as sa_select
from quart import jsonify from quart import jsonify
@@ -129,7 +129,7 @@ def register():
@require_admin @require_admin
async def update_sumup(slug: str): async def update_sumup(slug: str):
"""Update PageConfig SumUp credentials for a page.""" """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 sqlalchemy import select as sa_select
from quart import jsonify from quart import jsonify
@@ -191,7 +191,7 @@ def register():
@require_admin @require_admin
async def calendar_view(slug: str, calendar_id: int): async def calendar_view(slug: str, calendar_id: int):
"""Show calendar month view for browsing entries""" """Show calendar month view for browsing entries"""
from events.models.calendars import Calendar from shared.models.calendars import Calendar
from sqlalchemy import select from sqlalchemy import select
from datetime import datetime, timezone from datetime import datetime, timezone
from quart import request from quart import request
@@ -273,7 +273,7 @@ def register():
@require_admin @require_admin
async def entries(slug: str): async def entries(slug: str):
from ..services.entry_associations import get_post_entry_ids 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 from sqlalchemy import select
post_id = g.post_data["post"]["id"] post_id = g.post_data["post"]["id"]
@@ -309,7 +309,7 @@ def register():
@require_admin @require_admin
async def toggle_entry(slug: str, entry_id: int): 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 ..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 sqlalchemy import select
from quart import jsonify from quart import jsonify
@@ -603,7 +603,7 @@ def register():
@require_admin @require_admin
async def markets(slug: str): async def markets(slug: str):
"""List markets for this page.""" """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 from sqlalchemy import select as sa_select
post = (g.post_data or {}).get("post", {}) post = (g.post_data or {}).get("post", {})
@@ -631,7 +631,7 @@ def register():
async def create_market(slug: str): async def create_market(slug: str):
"""Create a new market for this page.""" """Create a new market for this page."""
from ..services.markets import create_market as _create_market, MarketError 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 sqlalchemy import select as sa_select
from quart import jsonify from quart import jsonify
@@ -669,7 +669,7 @@ def register():
async def delete_market(slug: str, market_slug: str): async def delete_market(slug: str, market_slug: str):
"""Soft-delete a market.""" """Soft-delete a market."""
from ..services.markets import soft_delete_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 sqlalchemy import select as sa_select
from quart import jsonify from quart import jsonify

View File

@@ -11,8 +11,8 @@ from quart import (
) )
from .services.post_data import post_data from .services.post_data import post_data
from .services.post_operations import toggle_post_like from .services.post_operations import toggle_post_like
from events.models.calendars import Calendar from shared.models.calendars import Calendar
from market.models.market_place import MarketPlace from shared.models.market_place import MarketPlace
from sqlalchemy import select from sqlalchemy import select
from shared.browser.app.redis_cacher import cache_page, clear_cache from shared.browser.app.redis_cacher import cache_page, clear_cache

View File

@@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.sql import func 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 from models.ghost_content import Post

View File

@@ -6,9 +6,9 @@ import unicodedata
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession 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 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 shared.browser.app.utils import utcnow
from glue.services.relationships import attach_child, detach_child from glue.services.relationships import attach_child, detach_child

2
glue

Submodule glue updated: fc14d8323a...ebce44e9d9

View File

@@ -1,216 +1,3 @@
from datetime import datetime from shared.models.ghost_content import ( # noqa: F401
from typing import List, Optional Tag, Post, Author, PostAuthor, PostTag, PostLike,
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[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")

2
shared

Submodule shared updated: da10fc4cf9...0c0f3c8416