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>
84 lines
2.5 KiB
Python
84 lines
2.5 KiB
Python
"""
|
|
Internal JSON API for the coop app.
|
|
|
|
These endpoints are called by other apps (market, cart) over HTTP
|
|
to fetch Ghost CMS content and menu items without importing blog services.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from quart import Blueprint, g, jsonify
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
from shared.models.menu_item import MenuItem
|
|
from shared.browser.app.csrf import csrf_exempt
|
|
|
|
|
|
def register() -> Blueprint:
|
|
bp = Blueprint("coop_api", __name__, url_prefix="/internal")
|
|
|
|
@bp.get("/menu-items")
|
|
@csrf_exempt
|
|
async def menu_items():
|
|
"""
|
|
Return all active menu items as lightweight JSON.
|
|
Called by market and cart apps to render the nav.
|
|
"""
|
|
result = await g.s.execute(
|
|
select(MenuItem)
|
|
.where(MenuItem.deleted_at.is_(None))
|
|
.options(selectinload(MenuItem.post))
|
|
.order_by(MenuItem.sort_order.asc(), MenuItem.id.asc())
|
|
)
|
|
items = result.scalars().all()
|
|
|
|
return jsonify(
|
|
[
|
|
{
|
|
"id": mi.id,
|
|
"post": {
|
|
"title": mi.post.title if mi.post else None,
|
|
"slug": mi.post.slug if mi.post else None,
|
|
"feature_image": mi.post.feature_image if mi.post else None,
|
|
},
|
|
}
|
|
for mi in items
|
|
]
|
|
)
|
|
|
|
@bp.get("/post/<slug>")
|
|
@csrf_exempt
|
|
async def post_by_slug(slug: str):
|
|
"""
|
|
Return a Ghost post's key fields by slug.
|
|
Called by market app for the landing page.
|
|
"""
|
|
from bp.blog.ghost_db import DBClient
|
|
|
|
client = DBClient(g.s)
|
|
posts = await client.posts_by_slug(slug, include_drafts=False)
|
|
|
|
if not posts:
|
|
return jsonify(None), 404
|
|
|
|
post, original_post = posts[0]
|
|
|
|
return jsonify(
|
|
{
|
|
"post": {
|
|
"id": post.get("id"),
|
|
"title": post.get("title"),
|
|
"html": post.get("html"),
|
|
"custom_excerpt": post.get("custom_excerpt"),
|
|
"feature_image": post.get("feature_image"),
|
|
"slug": post.get("slug"),
|
|
},
|
|
"original_post": {
|
|
"id": getattr(original_post, "id", None),
|
|
"title": getattr(original_post, "title", None),
|
|
},
|
|
}
|
|
)
|
|
|
|
return bp
|