feat: decouple blog from shared_lib, add app-owned models
Phase 1-3 of decoupling: - path_setup.py adds project root to sys.path - Blog-owned models in blog/models/ (ghost_content, snippet, tag_group) - Re-export shims for shared models (user, kv, magic_link, menu_item) - All imports updated: shared.infrastructure, shared.db, shared.browser, etc. - No more cross-app post_id FKs in calendar/market/page_config Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,9 +10,9 @@ from quart import (
|
||||
redirect,
|
||||
url_for,
|
||||
)
|
||||
from suma_browser.app.authz import require_admin, require_post_author
|
||||
from suma_browser.app.utils.htmx import is_htmx_request
|
||||
from utils import host_url
|
||||
from shared.browser.app.authz import require_admin, require_post_author
|
||||
from shared.browser.app.utils.htmx import is_htmx_request
|
||||
from shared.utils import host_url
|
||||
|
||||
def register():
|
||||
bp = Blueprint("admin", __name__, url_prefix='/admin')
|
||||
@@ -21,8 +21,8 @@ def register():
|
||||
@bp.get("/")
|
||||
@require_admin
|
||||
async def admin(slug: str):
|
||||
from suma_browser.app.utils.htmx import is_htmx_request
|
||||
from models.page_config import PageConfig
|
||||
from shared.browser.app.utils.htmx import is_htmx_request
|
||||
from cart.models.page_config import PageConfig
|
||||
from sqlalchemy import select as sa_select
|
||||
|
||||
# Load features for page admin
|
||||
@@ -33,7 +33,7 @@ def register():
|
||||
sumup_checkout_prefix = ""
|
||||
if post.get("is_page"):
|
||||
pc = (await g.s.execute(
|
||||
sa_select(PageConfig).where(PageConfig.post_id == post["id"])
|
||||
sa_select(PageConfig).where(PageConfig.container_type == "page", PageConfig.container_id == post["id"])
|
||||
)).scalar_one_or_none()
|
||||
if pc:
|
||||
features = pc.features or {}
|
||||
@@ -62,7 +62,7 @@ def register():
|
||||
@require_admin
|
||||
async def update_features(slug: str):
|
||||
"""Update PageConfig.features for a page."""
|
||||
from models.page_config import PageConfig
|
||||
from cart.models.page_config import PageConfig
|
||||
from models.ghost_content import Post
|
||||
from sqlalchemy import select as sa_select
|
||||
from quart import jsonify
|
||||
@@ -76,10 +76,10 @@ def register():
|
||||
|
||||
# Load or create PageConfig
|
||||
pc = (await g.s.execute(
|
||||
sa_select(PageConfig).where(PageConfig.post_id == post_id)
|
||||
sa_select(PageConfig).where(PageConfig.container_type == "page", PageConfig.container_id == post_id)
|
||||
)).scalar_one_or_none()
|
||||
if pc is None:
|
||||
pc = PageConfig(post_id=post_id, features={})
|
||||
pc = PageConfig(container_type="page", container_id=post_id, features={})
|
||||
g.s.add(pc)
|
||||
await g.s.flush()
|
||||
|
||||
@@ -127,7 +127,7 @@ def register():
|
||||
@require_admin
|
||||
async def update_sumup(slug: str):
|
||||
"""Update PageConfig SumUp credentials for a page."""
|
||||
from models.page_config import PageConfig
|
||||
from cart.models.page_config import PageConfig
|
||||
from sqlalchemy import select as sa_select
|
||||
from quart import jsonify
|
||||
|
||||
@@ -138,10 +138,10 @@ def register():
|
||||
post_id = post["id"]
|
||||
|
||||
pc = (await g.s.execute(
|
||||
sa_select(PageConfig).where(PageConfig.post_id == post_id)
|
||||
sa_select(PageConfig).where(PageConfig.container_type == "page", PageConfig.container_id == post_id)
|
||||
)).scalar_one_or_none()
|
||||
if pc is None:
|
||||
pc = PageConfig(post_id=post_id, features={})
|
||||
pc = PageConfig(container_type="page", container_id=post_id, features={})
|
||||
g.s.add(pc)
|
||||
await g.s.flush()
|
||||
|
||||
@@ -187,7 +187,7 @@ def register():
|
||||
@require_admin
|
||||
async def calendar_view(slug: str, calendar_id: int):
|
||||
"""Show calendar month view for browsing entries"""
|
||||
from models.calendars import Calendar
|
||||
from events.models.calendars import Calendar
|
||||
from sqlalchemy import select
|
||||
from datetime import datetime, timezone
|
||||
from quart import request
|
||||
@@ -269,7 +269,7 @@ def register():
|
||||
@require_admin
|
||||
async def entries(slug: str):
|
||||
from ..services.entry_associations import get_post_entry_ids
|
||||
from models.calendars import Calendar
|
||||
from events.models.calendars import Calendar
|
||||
from sqlalchemy import select
|
||||
|
||||
post_id = g.post_data["post"]["id"]
|
||||
@@ -305,7 +305,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 models.calendars import Calendar
|
||||
from events.models.calendars import Calendar
|
||||
from sqlalchemy import select
|
||||
from quart import jsonify
|
||||
|
||||
@@ -339,7 +339,7 @@ def register():
|
||||
calendars = (
|
||||
await g.s.execute(
|
||||
select(Calendar)
|
||||
.where(Calendar.post_id == post_id, Calendar.deleted_at.is_(None))
|
||||
.where(Calendar.container_type == "page", Calendar.container_id == post_id, Calendar.deleted_at.is_(None))
|
||||
.order_by(Calendar.name.asc())
|
||||
)
|
||||
).scalars().all()
|
||||
@@ -389,7 +389,7 @@ def register():
|
||||
async def settings_save(slug: str):
|
||||
from ...blog.ghost.ghost_posts import update_post_settings
|
||||
from ...blog.ghost.ghost_sync import sync_single_post
|
||||
from suma_browser.app.redis_cacher import invalidate_tag_cache
|
||||
from shared.browser.app.redis_cacher import invalidate_tag_cache
|
||||
|
||||
ghost_id = g.post_data["post"]["ghost_id"]
|
||||
form = await request.form
|
||||
@@ -452,7 +452,7 @@ def register():
|
||||
@require_post_author
|
||||
async def edit(slug: str):
|
||||
from ...blog.ghost.ghost_posts import get_post_for_edit
|
||||
from models.ghost_membership_entities import GhostNewsletter
|
||||
from shared.models.ghost_membership_entities import GhostNewsletter
|
||||
from sqlalchemy import select as sa_select
|
||||
|
||||
ghost_id = g.post_data["post"]["ghost_id"]
|
||||
@@ -487,7 +487,7 @@ def register():
|
||||
from ...blog.ghost.ghost_posts import update_post
|
||||
from ...blog.ghost.lexical_validator import validate_lexical
|
||||
from ...blog.ghost.ghost_sync import sync_single_post
|
||||
from suma_browser.app.redis_cacher import invalidate_tag_cache
|
||||
from shared.browser.app.redis_cacher import invalidate_tag_cache
|
||||
|
||||
ghost_id = g.post_data["post"]["ghost_id"]
|
||||
form = await request.form
|
||||
@@ -599,7 +599,7 @@ def register():
|
||||
@require_admin
|
||||
async def markets(slug: str):
|
||||
"""List markets for this page."""
|
||||
from models.market_place import MarketPlace
|
||||
from market.models.market_place import MarketPlace
|
||||
from sqlalchemy import select as sa_select
|
||||
|
||||
post = (g.post_data or {}).get("post", {})
|
||||
@@ -609,7 +609,8 @@ def register():
|
||||
|
||||
page_markets = (await g.s.execute(
|
||||
sa_select(MarketPlace).where(
|
||||
MarketPlace.post_id == post_id,
|
||||
MarketPlace.container_type == "page",
|
||||
MarketPlace.container_id == post_id,
|
||||
MarketPlace.deleted_at.is_(None),
|
||||
).order_by(MarketPlace.name)
|
||||
)).scalars().all()
|
||||
@@ -626,7 +627,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 models.market_place import MarketPlace
|
||||
from market.models.market_place import MarketPlace
|
||||
from sqlalchemy import select as sa_select
|
||||
from quart import jsonify
|
||||
|
||||
@@ -646,7 +647,8 @@ def register():
|
||||
# Return updated markets list
|
||||
page_markets = (await g.s.execute(
|
||||
sa_select(MarketPlace).where(
|
||||
MarketPlace.post_id == post_id,
|
||||
MarketPlace.container_type == "page",
|
||||
MarketPlace.container_id == post_id,
|
||||
MarketPlace.deleted_at.is_(None),
|
||||
).order_by(MarketPlace.name)
|
||||
)).scalars().all()
|
||||
@@ -663,7 +665,7 @@ def register():
|
||||
async def delete_market(slug: str, market_slug: str):
|
||||
"""Soft-delete a market."""
|
||||
from ..services.markets import soft_delete_market
|
||||
from models.market_place import MarketPlace
|
||||
from market.models.market_place import MarketPlace
|
||||
from sqlalchemy import select as sa_select
|
||||
from quart import jsonify
|
||||
|
||||
@@ -677,7 +679,8 @@ def register():
|
||||
# Return updated markets list
|
||||
page_markets = (await g.s.execute(
|
||||
sa_select(MarketPlace).where(
|
||||
MarketPlace.post_id == post_id,
|
||||
MarketPlace.container_type == "page",
|
||||
MarketPlace.container_id == post_id,
|
||||
MarketPlace.deleted_at.is_(None),
|
||||
).order_by(MarketPlace.name)
|
||||
)).scalars().all()
|
||||
|
||||
@@ -11,16 +11,16 @@ from quart import (
|
||||
)
|
||||
from .services.post_data import post_data
|
||||
from .services.post_operations import toggle_post_like
|
||||
from models.calendars import Calendar
|
||||
from models.market_place import MarketPlace
|
||||
from events.models.calendars import Calendar
|
||||
from market.models.market_place import MarketPlace
|
||||
from sqlalchemy import select
|
||||
|
||||
from suma_browser.app.redis_cacher import cache_page, clear_cache
|
||||
from shared.browser.app.redis_cacher import cache_page, clear_cache
|
||||
|
||||
|
||||
from .admin.routes import register as register_admin
|
||||
from config import config
|
||||
from suma_browser.app.utils.htmx import is_htmx_request
|
||||
from shared.config import config
|
||||
from shared.browser.app.utils.htmx import is_htmx_request
|
||||
|
||||
def register():
|
||||
bp = Blueprint("post", __name__, url_prefix='/<slug>')
|
||||
@@ -65,13 +65,13 @@ def register():
|
||||
p_data = getattr(g, "post_data", None)
|
||||
if p_data:
|
||||
from .services.entry_associations import get_associated_entries
|
||||
from shared.internal_api import get as api_get
|
||||
from shared.infrastructure.internal_api import get as api_get
|
||||
|
||||
db_post_id = (g.post_data.get("post") or {}).get("id") # <-- integer
|
||||
calendars = (
|
||||
await g.s.execute(
|
||||
select(Calendar)
|
||||
.where(Calendar.post_id == db_post_id, Calendar.deleted_at.is_(None))
|
||||
.where(Calendar.container_type == "page", Calendar.container_id == db_post_id, Calendar.deleted_at.is_(None))
|
||||
.order_by(Calendar.name.asc())
|
||||
)
|
||||
).scalars().all()
|
||||
@@ -79,7 +79,7 @@ def register():
|
||||
markets = (
|
||||
await g.s.execute(
|
||||
select(MarketPlace)
|
||||
.where(MarketPlace.post_id == db_post_id, MarketPlace.deleted_at.is_(None))
|
||||
.where(MarketPlace.container_type == "page", MarketPlace.container_id == db_post_id, MarketPlace.deleted_at.is_(None))
|
||||
.order_by(MarketPlace.name.asc())
|
||||
)
|
||||
).scalars().all()
|
||||
@@ -130,7 +130,7 @@ def register():
|
||||
@bp.post("/like/toggle/")
|
||||
@clear_cache(tag="post.post_detail", tag_scope="user")
|
||||
async def like_toggle(slug: str):
|
||||
from utils import host_url
|
||||
from shared.utils import host_url
|
||||
|
||||
# Get post_id from g.post_data
|
||||
if not g.user:
|
||||
|
||||
@@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from models.calendars import CalendarEntry, CalendarEntryPost, Calendar
|
||||
from events.models.calendars import CalendarEntry, CalendarEntryPost, Calendar
|
||||
from models.ghost_content import Post
|
||||
|
||||
|
||||
@@ -35,7 +35,8 @@ async def toggle_entry_association(
|
||||
existing = await session.scalar(
|
||||
select(CalendarEntryPost).where(
|
||||
CalendarEntryPost.entry_id == entry_id,
|
||||
CalendarEntryPost.post_id == post_id,
|
||||
CalendarEntryPost.content_type == "post",
|
||||
CalendarEntryPost.content_id == post_id,
|
||||
CalendarEntryPost.deleted_at.is_(None)
|
||||
)
|
||||
)
|
||||
@@ -49,7 +50,8 @@ async def toggle_entry_association(
|
||||
# Create association
|
||||
association = CalendarEntryPost(
|
||||
entry_id=entry_id,
|
||||
post_id=post_id
|
||||
content_type="post",
|
||||
content_id=post_id
|
||||
)
|
||||
session.add(association)
|
||||
await session.flush()
|
||||
@@ -67,7 +69,8 @@ async def get_post_entry_ids(
|
||||
result = await session.execute(
|
||||
select(CalendarEntryPost.entry_id)
|
||||
.where(
|
||||
CalendarEntryPost.post_id == post_id,
|
||||
CalendarEntryPost.content_type == "post",
|
||||
CalendarEntryPost.content_id == post_id,
|
||||
CalendarEntryPost.deleted_at.is_(None)
|
||||
)
|
||||
)
|
||||
@@ -88,7 +91,8 @@ async def get_associated_entries(
|
||||
entry_ids_result = await session.execute(
|
||||
select(CalendarEntryPost.entry_id)
|
||||
.where(
|
||||
CalendarEntryPost.post_id == post_id,
|
||||
CalendarEntryPost.content_type == "post",
|
||||
CalendarEntryPost.content_id == post_id,
|
||||
CalendarEntryPost.deleted_at.is_(None)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -6,10 +6,10 @@ import unicodedata
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models.market_place import MarketPlace
|
||||
from market.models.market_place import MarketPlace
|
||||
from models.ghost_content import Post
|
||||
from models.page_config import PageConfig
|
||||
from suma_browser.app.utils import utcnow
|
||||
from cart.models.page_config import PageConfig
|
||||
from shared.browser.app.utils import utcnow
|
||||
|
||||
|
||||
class MarketError(ValueError):
|
||||
@@ -43,14 +43,14 @@ async def create_market(sess: AsyncSession, post_id: int, name: str) -> MarketPl
|
||||
raise MarketError("Markets can only be created on pages, not posts.")
|
||||
|
||||
pc = (await sess.execute(
|
||||
select(PageConfig).where(PageConfig.post_id == post_id)
|
||||
select(PageConfig).where(PageConfig.container_type == "page", PageConfig.container_id == post_id)
|
||||
)).scalar_one_or_none()
|
||||
if pc is None or not (pc.features or {}).get("market"):
|
||||
raise MarketError("Market feature is not enabled for this page. Enable it in page settings first.")
|
||||
|
||||
# Look for existing (including soft-deleted)
|
||||
existing = (await sess.execute(
|
||||
select(MarketPlace).where(MarketPlace.post_id == post_id, MarketPlace.slug == slug)
|
||||
select(MarketPlace).where(MarketPlace.container_type == "page", MarketPlace.container_id == post_id, MarketPlace.slug == slug)
|
||||
)).scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
@@ -61,7 +61,7 @@ async def create_market(sess: AsyncSession, post_id: int, name: str) -> MarketPl
|
||||
return existing
|
||||
raise MarketError(f'Market with slug "{slug}" already exists for this page.')
|
||||
|
||||
market = MarketPlace(post_id=post_id, name=name, slug=slug)
|
||||
market = MarketPlace(container_type="page", container_id=post_id, name=name, slug=slug)
|
||||
sess.add(market)
|
||||
await sess.flush()
|
||||
return market
|
||||
@@ -71,7 +71,8 @@ async def soft_delete_market(sess: AsyncSession, post_slug: str, market_slug: st
|
||||
market = (
|
||||
await sess.execute(
|
||||
select(MarketPlace)
|
||||
.join(Post, MarketPlace.post_id == Post.id)
|
||||
.join(Post, MarketPlace.container_id == Post.id)
|
||||
.where(MarketPlace.container_type == "page")
|
||||
.where(
|
||||
Post.slug == post_slug,
|
||||
MarketPlace.slug == market_slug,
|
||||
|
||||
Reference in New Issue
Block a user